Pseudo-class vs Pseudo-element
These two look similar but do very different things:
| Type | Syntax | What it does | Examples |
|---|---|---|---|
| Pseudo-class | Single colon : | Selects elements in a state or position | :hover, :focus, :nth-child() |
| Pseudo-element | Double colon :: | Selects a part of an element or inserts virtual content | ::before, ::after, ::placeholder |
Note: Browsers accept single colon for pseudo-elements (:before) for backwards compatibility, but the modern standard uses double colon (::before). Always use :: in new code.
Common Pseudo-classes
:hoverElement is being hovered by the mouse pointer.
:focusElement has keyboard or programmatic focus.
:activeElement is being clicked/pressed.
:visitedLink that the user has already visited.
:linkLink that has NOT been visited.
:not(selector)Every element that does NOT match the selector.
/* Hover effect on button */
.btn {
background: #6d28d9;
transition: background 0.2s, transform 0.15s;
}
.btn:hover {
background: #5b21b6;
transform: translateY(-2px);
}
/* Visible focus for accessibility */
.btn:focus-visible {
outline: 3px solid #a78bfa;
outline-offset: 2px;
}
/* Style unvisited vs visited links */
a:link { color: #6d28d9; }
a:visited { color: #9333ea; }
/* Every li except the last one */
li:not(:last-child) {
border-bottom: 1px solid #e0ddf7;
}Structural Pseudo-classes
These select elements based on their position among siblings.
:first-child, :last-child, :only-child
/* First list item */
li:first-child { font-weight: 700; }
/* Last table row */
tr:last-child td { border-bottom: none; }
/* Card that is an only child */
.card:only-child { margin: 0 auto; }:nth-child() and :nth-of-type()
These accept a formula An+B where A is the step and B is the offset.
/* Every even row — table striping */
tr:nth-child(even) { background: #f8f7ff; }
/* Every 3rd item starting from the 1st */
li:nth-child(3n+1) { color: #6d28d9; }
/* Odd rows */
tr:nth-child(odd) { background: #fff; }
/* Last 2 items */
li:nth-last-child(-n+2) { opacity: 0.5; }
/* 3rd paragraph specifically */
p:nth-of-type(3) { font-size: 1.1rem; font-weight: 600; }Keyword shortcuts: :nth-child(even) = :nth-child(2n), :nth-child(odd) = :nth-child(2n+1).
Form Pseudo-classes
These are especially useful for form validation UI without any JavaScript.
/* Input that has been filled */
input:not(:placeholder-shown) {
border-color: #6d28d9;
}
/* Valid input */
input:valid {
border-color: #22c55e;
background: #f0fdf4;
}
/* Invalid input (only after user interaction) */
input:invalid:not(:placeholder-shown) {
border-color: #ef4444;
background: #fef2f2;
}
/* Disabled state */
input:disabled {
background: #f1f5f9;
cursor: not-allowed;
opacity: 0.6;
}
/* Checked checkbox */
input[type="checkbox"]:checked + label {
font-weight: 700;
color: #6d28d9;
}
/* Required fields */
input:required {
border-left: 3px solid #f59e0b;
}Pseudo-elements
::beforeInserts content before the element's content.
::afterInserts content after the element's content.
::placeholderStyles the placeholder text of an input.
::selectionStyles text selected by the user.
::first-lineStyles the first line of a block element.
::first-letterStyles the first letter (drop cap effect).
/* Custom placeholder */
input::placeholder {
color: #94a3b8;
font-style: italic;
}
/* Selected text color */
::selection {
background: #6d28d9;
color: #fff;
}
/* Drop cap */
article p:first-of-type::first-letter {
font-size: 3em;
font-weight: 900;
float: left;
line-height: 1;
margin-right: 0.1em;
color: #6d28d9;
}::before and ::after in Depth
These are the most powerful pseudo-elements. They inject a virtual child element before or after the element's real content. The content property is required (even if empty).
/* Decorative quotation marks */
blockquote::before {
content: '\201C'; /* " */
font-size: 4em;
color: #6d28d9;
line-height: 0;
vertical-align: -0.4em;
margin-right: 0.1em;
}
/* Badge / counter on icon */
.cart-icon {
position: relative;
display: inline-block;
}
.cart-icon::after {
content: attr(data-count); /* reads HTML attribute */
position: absolute;
top: -6px;
right: -8px;
background: #ef4444;
color: #fff;
font-size: .65rem;
font-weight: 700;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/* Gradient underline on heading */
.fancy-heading::after {
content: '';
display: block;
width: 60px;
height: 4px;
border-radius: 2px;
background: linear-gradient(90deg, #6d28d9, #f59e0b);
margin-top: 0.5rem;
}
/* Custom checkbox */
.custom-check input:checked + label::before {
content: '\2713'; /* ✓ */
background: #6d28d9;
color: #fff;
}Trick: Use content: attr(data-x) to read any HTML attribute value directly into a pseudo-element — great for tooltips and badges.
Chaining and Combining
Pseudo-classes can be chained together and combined with pseudo-elements.
/* Last child that is also hovered */
li:last-child:hover { background: #f0edfb; }
/* Not the first child, not disabled */
button:not(:first-child):not(:disabled) { margin-left: .5rem; }
/* ::before on hovered element */
.nav-link:hover::before {
content: '';
display: block;
width: 100%;
height: 2px;
background: #6d28d9;
}
/* nth-child with pseudo-element */
li:nth-child(odd)::before {
content: '▶ ';
color: #6d28d9;
font-size: .7em;
}Practical Patterns
Animated underline on links
.link {
position: relative;
text-decoration: none;
color: #6d28d9;
}
.link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: #6d28d9;
transition: width 0.25s ease;
}
.link:hover::after { width: 100%; }Zebra striped table without JS
table tr:nth-child(even) td { background: #f8f7ff; }
table tr:hover td { background: #ede9fe; }Input floating label (CSS-only)
.field { position: relative; }
.field label {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
transition: all 0.2s;
color: #94a3b8;
pointer-events: none;
}
.field input:focus + label,
.field input:not(:placeholder-shown) + label {
top: 0;
font-size: .72rem;
color: #6d28d9;
background: #fff;
padding: 0 .25rem;
}