CSS Pseudo-Classes and Pseudo-Elements
Learn the difference between pseudo-classes and pseudo-elements with practical examples like hover, focus-visible, before, and placeholder.
What you'll learn
- ✓The difference between :pseudo-class and ::pseudo-element
- ✓Hover, focus, and focus-visible patterns
- ✓Generated content with before and after
- ✓Form-specific pseudos like placeholder and invalid
- ✓Specificity considerations
Prerequisites
- •Comfortable with HTML and JavaScript
What and Why
CSS gives you two extension points beyond plain elements. Pseudo-classes select an element based on its state, like being hovered or focused. Pseudo-elements style a sub-part of an element, like its first line or a generated marker. Both let you keep the HTML simple while moving presentation into the stylesheet.
Mental Model
Pseudo-classes use a single colon (:hover, :focus, :checked). They are conditions: apply this rule when the element is in this state. Pseudo-elements use two colons (::before, ::placeholder, ::first-line). They are tiny virtual children of an element you can style independently.
a:hover -> rule applies when anchor is hovered
input:invalid -> rule applies when input fails validation
p::first-line -> styles the first visual line of the paragraph
button::before -> inserts a generated child before the content
Hands-on Example
Pseudo-classes for interactivity.
button {
background: #1e293b;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
}
button:hover {
background: #334155;
}
button:focus-visible {
outline: 3px solid #38bdf8;
outline-offset: 2px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
:focus-visible is the modern alternative to :focus. It only shows the outline when the user navigates with the keyboard, which removes the ring on mouse clicks while keeping accessibility intact.
Pseudo-elements for decoration.
.badge {
position: relative;
padding-left: 1.25rem;
}
.badge::before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 0.5rem;
height: 0.5rem;
background: #22c55e;
border-radius: 9999px;
transform: translateY(-50%);
}
The content property is what makes ::before and ::after render. Without it the pseudo-element has no box. Use an empty string for decorative dots and pass real text for tooltips or section labels.
Form pseudo-elements style controls without wrapping HTML.
input::placeholder {
color: #94a3b8;
font-style: italic;
}
input:invalid:not(:placeholder-shown) {
border-color: #ef4444;
}
input:valid {
border-color: #22c55e;
}
The :not(:placeholder-shown) part avoids painting the field red before the user has typed anything. It is a small detail that makes the form feel forgiving instead of nagging.
For lists, the modern ::marker pseudo-element lets you style bullets without removing them.
ul li::marker {
color: #2563eb;
font-weight: bold;
}
Common Pitfalls
People often confuse the colons. The HTML spec accepts :before and :after for backwards compatibility, but every new pseudo-element introduced since CSS3 requires the double colon. Use ::before and ::after consistently to match modern conventions.
Pseudo-elements need a content property to appear, even if empty. Forgetting it is the most common reason a ::before rule does nothing.
Pseudo-classes can stack. The order matters because they all have the same specificity. The rule that comes last wins among ties, which is why :hover should come before :focus-visible in your stylesheet if you want focus styles to win when both are true.
:hover is a hostile pattern on touch devices because there is no hover state. Make sure your interactive elements work on tap and that critical content does not hide behind a hover. Pair :hover with :focus-within for menus that should also open via keyboard.
Practical Tips
Use :focus-visible everywhere you previously used :focus. It is supported in every modern browser and avoids the ugly outline on mouse clicks while keeping the indicator for keyboard users.
The :has() pseudo-class lets you style a parent based on its children, which used to require JavaScript. form:has(input:invalid) styles a form whose any input is invalid. Combine it with :not() for complex states.
form:has(input:invalid) button[type="submit"] {
opacity: 0.5;
pointer-events: none;
}
Pseudo-elements are great for icons that should not be in the accessibility tree. Decorative bullets via ::before are invisible to screen readers, while inline SVG would announce as graphic. Use the right tool for content vs decoration.
Specificity for pseudo-classes is the same as a class selector. Pseudo-elements have element specificity. Knowing this helps when a rule does not apply: a stronger selector elsewhere may be winning.
Wrap-up
Pseudo-classes and pseudo-elements unlock most of CSS’s expressive power without changing your HTML. Use the single colon for state, the double colon for sub-parts, and reach for the newer additions like :focus-visible, ::marker, and :has() to write less JavaScript and more declarative styles.
Related articles
- HTML & CSS CSS :has() Pseudo-Class Tutorial
Learn how the CSS :has() pseudo-class enables true parent selectors, conditional styling, and sibling-aware rules. Includes a mental model, examples, pitfalls, and best practices for production use.
- HTML & CSS CSS Selectors and Specificity Explained
Selectors decide which elements get styled; specificity decides which rule wins when more than one applies. This guide covers every selector you need and how to read a specificity score.
- HTML & CSS CSS Animations and Keyframes Explained
How CSS keyframe animations actually work: timing functions, fill modes, composition, and the patterns that keep them smooth and predictable.
- HTML & CSS CSS Cascade Layers and Specificity: A Calmer Mental Model
Stop fighting !important. Learn how the CSS cascade, specificity, and the new @layer rule combine to give you predictable, maintainable styles.