Skip to content
C Codeloom
HTML & CSS

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.

·4 min read · By Codeloom
Beginner 8 min read

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
Single colon vs double colon

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.