CSS Reference Guide

All TopicsPlayground

Pseudo-Classes & Pseudo-Elements

Select elements based on state, position, and structure — then style parts of elements that don't exist in the DOM. Pseudo-classes and pseudo-elements unlock powerful styling without extra markup.

SelectorsPseudo-ClassesPseudo-ElementsInteractiveStructural

User-Action Pseudo-Classes

These pseudo-classes respond to how the user interacts with elements. They are the foundation of interactive CSS — enabling hover effects, active states, focus rings, and visited link styling.

:hover

Applies when the user's pointer is over the element. The most commonly used interactive pseudo-class.

CSS
a:hover {
  color: coral;
  text-decoration: underline;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
Live Example — :hover Effect
Hover!

:active

Applies while an element is being activated — typically while a mouse button is pressed down on it.

CSS
.btn:active {
  transform: scale(0.95);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
}
Live Example — :active Press Effect

:link & :visited

:link selects unvisited links. :visited selects links that have been visited. Together they let you style links based on browsing history.

CSS
a:link {
  color: #6c63ff;
}

a:visited {
  color: #9b59b6;
}

/* LVHA order: :link, :visited, :hover, :active */
LVHA Order

Always declare link pseudo-classes in L-V-H-A order: :link, :visited, :hover, :active. Because of CSS specificity rules, changing this order can cause unexpected overrides. A helpful mnemonic: "Love Hate" (LoVe HAte).

:focus

Applies when an element has received focus — by clicking, tapping, or tabbing to it. Essential for keyboard accessibility.

CSS
input:focus {
  outline: 2px solid #6c63ff;
  outline-offset: 2px;
  border-color: #6c63ff;
}
Live Example — :focus Styling

:focus-visible

Like :focus, but only applies when focus should be visually indicated — typically for keyboard navigation, not mouse clicks. This is the modern best practice for focus rings.

CSS
/* Remove default focus ring for mouse users */
.btn:focus {
  outline: none;
}

/* Show focus ring only for keyboard users */
.btn:focus-visible {
  outline: 2px solid #6c63ff;
  outline-offset: 3px;
}
Modern Focus Best Practice

Use :focus-visible instead of :focus for focus ring styling. This way, mouse users won't see the focus ring, but keyboard users will still get clear visual feedback. It's the best of both worlds for accessibility and aesthetics.

Live Example — :focus-visible (try tabbing vs clicking)

Click a button — no outline. Now use Tab — you'll see the green focus ring appear.

Structural Pseudo-Classes

Structural pseudo-classes select elements based on their position in the DOM tree — their relationship to parents, siblings, and the document itself.

:first-child & :last-child

Select the first or last child element within a parent container.

CSS
li:first-child {
  font-weight: bold;
  color: #6c63ff;
}

li:last-child {
  border-bottom: none;
}
Live Example — :first-child & :last-child
  • First Item (styled)
  • Second Item
  • Third Item
  • Fourth Item
  • Last Item (styled)

:only-child

Matches an element that is the sole child of its parent — no siblings at all.

CSS
p:only-child {
  font-size: 1.25rem;
  text-align: center;
}

The :nth-child() an+b Formula

:nth-child() is one of the most powerful structural selectors. It accepts a formula of the form an+b where a is the cycle size and b is the offset.

CSS
/* Every odd element */
tr:nth-child(odd) {
  background: rgba(108, 99, 255, 0.08);
}

/* Every even element */
tr:nth-child(even) {
  background: rgba(108, 99, 255, 0.04);
}

/* Every 3rd element */
li:nth-child(3n) {
  color: #00c9a7;
}

/* Every 3rd, starting at 1st (1, 4, 7, 10...) */
li:nth-child(3n+1) {
  color: #6c63ff;
}

/* First 3 items only */
li:nth-child(-n+3) {
  font-weight: bold;
}

Understanding the Formula

Formula Matches Example Items
2n or evenEvery even child2, 4, 6, 8...
2n+1 or oddEvery odd child1, 3, 5, 7...
3nEvery 3rd3, 6, 9, 12...
3n+1Every 3rd from 1st1, 4, 7, 10...
n+44th and onward4, 5, 6, 7...
-n+3First 3 only1, 2, 3
5Only the 5th5
Live Example — :nth-child() with Different Formulas

:nth-child(3n+1) — highlighted in purple (items 1, 4, 7, 10):

1 2 3 4 5 6 7 8 9 10

:nth-child(even) — highlighted in teal (items 2, 4, 6, 8, 10):

1 2 3 4 5 6 7 8 9 10

:nth-child(-n+3) — first 3 items only:

1 2 3 4 5 6

:nth-of-type()

Like :nth-child() but only counts elements of the same type. Very useful when you have mixed element types inside a container.

CSS
/* Every odd paragraph, ignoring headings/divs */
p:nth-of-type(odd) {
  background: rgba(108, 99, 255, 0.1);
}

/* First heading of its type */
h3:first-of-type {
  margin-top: 0;
}
Live Example — :nth-child vs :nth-of-type

:nth-child(2) — 2nd child regardless of type

h2 (1st child)

p (2nd child - MATCH)

p (3rd child)

p:nth-of-type(2) — 2nd <p> element

h2 (skipped, not a p)

p (1st of type)

p (2nd of type - MATCH)

:nth-last-child() & :nth-last-of-type()

Same as their counterparts but count from the end instead of the beginning.

CSS
/* Last 2 items */
li:nth-last-child(-n+2) {
  opacity: 0.6;
}

/* Second to last paragraph */
p:nth-last-of-type(2) {
  margin-bottom: 2rem;
}

Functional Pseudo-Classes: :not(), :is(), :where()

:not() — Negation

Excludes elements that match the given selector. Extremely useful for applying styles to "everything except..."

CSS
/* All inputs except submit buttons */
input:not([type="submit"]) {
  border: 1px solid #ccc;
  padding: 0.5rem;
}

/* All list items except the last */
li:not(:last-child) {
  border-bottom: 1px solid #eee;
}

/* Modern :not() accepts multiple selectors */
a:not(.nav-link, .logo) {
  text-decoration: underline;
}
Live Example — :not(:last-child) Borders
  • Apple
  • Banana
  • Cherry
  • Date (no bottom border)

:is() — Matches-Any

Groups selectors together. It takes the highest specificity of its arguments. Dramatically reduces repetition in complex selectors.

CSS
/* Without :is() — repetitive */
header a:hover,
nav a:hover,
footer a:hover {
  color: #6c63ff;
}

/* With :is() — clean and concise */
:is(header, nav, footer) a:hover {
  color: #6c63ff;
}

/* Nested grouping */
:is(h1, h2, h3):is(:hover, :focus) {
  color: #00c9a7;
}

:where() — Zero-Specificity Grouping

Identical to :is() in function, but its specificity is always zero. Perfect for defaults that are easy to override.

CSS
/* These styles are easy to override because :where() = 0 specificity */
:where(header, footer) a {
  color: inherit;
  text-decoration: none;
}

/* This will easily override the above */
a {
  color: #6c63ff; /* wins, because a > :where(...) a */
}
:is() vs :where() — Specificity Difference

:is(.card, #hero) p has the specificity of #hero p (1-0-1) because :is() takes the highest specificity argument. :where(.card, #hero) p has specificity of just p (0-0-1) because :where() always contributes zero. Use :is() for normal styling, :where() for easily-overridable defaults and utility layers.

The :has() Selector — The "Parent Selector"

:has() is the long-awaited "parent selector" of CSS. It selects an element based on what it contains. This was previously impossible in CSS.

CSS
/* Card that contains an image */
.card:has(img) {
  padding-top: 0;
}

/* Form with invalid inputs — highlight border */
form:has(input:invalid) {
  border-color: #ff6b6b;
}

/* Figure that has a figcaption */
figure:has(figcaption) {
  border: 1px solid #ddd;
  padding: 1rem;
}

/* Previous-sibling selector (h2 followed by p) */
h2:has(+ p) {
  margin-bottom: 0.5rem;
}

/* Highlight label when its input is focused */
label:has(+ input:focus) {
  color: #6c63ff;
}
Live Example — :has() Parent Selector
placeholder

Card with image (purple border via :has)

Card without image (default border)

:has() is Revolutionary

:has() can replace many JavaScript patterns. Need to style a parent based on child state? Need to select a previous sibling? Need conditional layouts? :has() does it all purely in CSS. It's supported in all modern browsers as of late 2023.

Form Pseudo-Classes

These pseudo-classes target form elements based on their current state — checked, disabled, valid, invalid, required, and more.

CSS
/* Checked checkbox/radio */
input:checked {
  accent-color: #6c63ff;
}

/* Disabled inputs */
input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Valid input (passes constraints) */
input:valid {
  border-color: #00c9a7;
}

/* Invalid input (fails constraints) */
input:invalid {
  border-color: #ff6b6b;
}

/* Required field */
input:required {
  border-left: 3px solid #6c63ff;
}

/* Placeholder currently shown */
input:placeholder-shown {
  border-style: dashed;
}
Live Example — Interactive Form States
Type an invalid email to see :invalid (red). Valid email turns :valid (green).
This field has a left purple border (:required).

Pseudo-Elements

Pseudo-elements let you style specific parts of an element, or insert content that doesn't exist in the HTML. They use double colons (::) to distinguish them from pseudo-classes (:).

Double Colon (::) vs Single Colon (:)

The CSS3 spec uses :: for pseudo-elements and : for pseudo-classes. Older pseudo-elements like ::before, ::after, ::first-line, and ::first-letter also accept single-colon syntax for backward compatibility, but always prefer the double-colon form in modern code.

::before & ::after

These insert a pseudo-element as the first child (::before) or last child (::after) of the selected element. The content property is required — without it, the pseudo-element won't render.

CSS
/* Decorative quote marks */
blockquote::before {
  content: "\201C"; /* left double quote */
  font-size: 3rem;
  color: #6c63ff;
  line-height: 0;
  vertical-align: -0.4em;
}

/* Required field asterisk */
.required::after {
  content: " *";
  color: #ff6b6b;
}

/* External link icon */
a[href^="http"]::after {
  content: " \2197"; /* north-east arrow */
  font-size: 0.8em;
}

/* Decorative underline */
.fancy-title::after {
  content: "";
  display: block;
  width: 60px;
  height: 3px;
  background: #6c63ff;
  margin-top: 0.5rem;
}
content is Required!

Both ::before and ::after require the content property. Even if you want an empty pseudo-element for purely decorative purposes (a shape, a line, a background), you must set content: "". Without it, the pseudo-element simply won't render.

Live Example — ::before & ::after in Action

Decorative quotes with ::before and ::after:

CSS pseudo-elements are like invisible helpers in your markup.

Gradient underline with ::after:

Section Title

Required asterisk with ::after:

Email Address

Tooltip with ::after + attr():

Hover over me for a tooltip

The content Property Values

CSS
/* String text */
content: "Hello";

/* HTML attribute value */
content: attr(data-label);

/* Unicode character */
content: "\2605"; /* star */

/* Image */
content: url("icon.svg");

/* Counter */
content: counter(section);

/* Combined */
content: "Chapter " counter(chapter) ": ";

/* Empty (for decorative pseudo-elements) */
content: "";

Text Pseudo-Elements

::first-letter

Selects the first letter of the first line of a block-level element. Commonly used for drop caps in magazine or editorial layouts.

CSS
.article::first-letter {
  font-size: 3.5em;
  font-weight: bold;
  float: left;
  line-height: 0.8;
  margin-right: 0.1em;
  color: #6c63ff;
}
Live Example — ::first-letter Drop Cap

Once upon a time, in a world of cascading style sheets, a developer discovered the power of pseudo-elements. They could now style parts of the page that didn't even exist in the HTML source code, creating beautiful drop caps and decorative flourishes.

::first-line

Selects the first rendered line of a block element. The selection adjusts dynamically as the viewport resizes.

CSS
p::first-line {
  font-weight: 600;
  font-variant: small-caps;
  color: #6c63ff;
}
Live Example — ::first-line Styling

The first line of this paragraph is automatically styled differently. Try resizing your browser window and watch how the styled text reflows — the ::first-line pseudo-element always applies to whatever text is currently on the first rendered line.

Other Pseudo-Elements

::placeholder

Styles the placeholder text inside input fields and textareas.

CSS
input::placeholder {
  color: #888;
  font-style: italic;
  opacity: 0.7;
}
Live Example — ::placeholder

::selection

Styles the portion of text that the user has selected/highlighted with their cursor.

CSS
::selection {
  background: #6c63ff;
  color: #fff;
}

.highlight::selection {
  background: #00c9a7;
  color: #000;
}
Live Example — ::selection

Select this text for a purple highlight.

Select this text for a teal highlight.

Select this text for a red highlight.

::marker

Styles the marker of list items — the bullet point or number. Supported properties include color, font-size, content, and several others.

CSS
li::marker {
  color: #6c63ff;
  font-weight: bold;
  font-size: 1.2em;
}

ol li::marker {
  color: #00c9a7;
}
Live Example — ::marker
  • Purple bullets
  • Custom color
  • Bold and larger
  1. Teal numbers
  2. Bold weight
  3. No extra markup

Common Gotchas

:: vs : Confusion

Pseudo-classes use a single colon (:hover, :nth-child()). Pseudo-elements use a double colon (::before, ::after). While browsers accept :before for backward compatibility, always use ::before in new code to make the distinction clear.

Forgetting content on ::before / ::after

The most common mistake: writing styles for ::before or ::after but forgetting to set content. Even for purely decorative elements (shapes, lines), you need content: "" (empty string). Without it, the pseudo-element doesn't exist in the render tree.

::before/::after Don't Work on Replaced Elements

Elements like <img>, <input>, <br>, and <hr> cannot have ::before or ::after pseudo-elements because they are replaced elements — their content is replaced by external resources and they don't have internal content trees.

:nth-child() vs :nth-of-type() Confusion

:nth-child(2) selects the 2nd child of its parent regardless of element type. p:nth-of-type(2) selects the 2nd <p> element. These can behave very differently when a container has mixed element types. When in doubt, use :nth-of-type() — it's usually what you actually want.

Specificity of :is() vs :where()

:is(.foo, #bar) takes the specificity of its most specific argument (#bar = 1-0-0). This can cause unexpected specificity bumps. If you need zero specificity, use :where() instead.

Pro Tips

Custom Checkbox with ::before

Hide the default checkbox with appearance: none, then use ::before on a label or wrapper to create a fully custom checkbox design. You can combine this with :checked to toggle the visual state.

CSS
/* Custom checkbox pattern */
input[type="checkbox"] {
  appearance: none;
  width: 20px;
  height: 20px;
  border: 2px solid #555;
  border-radius: 4px;
  position: relative;
  cursor: pointer;
}

input[type="checkbox"]::before {
  content: "\2714";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 14px;
  opacity: 0;
  transition: opacity 0.15s;
}

input[type="checkbox"]:checked {
  background: #6c63ff;
  border-color: #6c63ff;
}

input[type="checkbox"]:checked::before {
  opacity: 1;
}
Use :is() to DRY Up Stylesheets

Whenever you find yourself writing the same styles for multiple selectors, reach for :is(). It can eliminate dozens of lines of repetitive CSS. For example, :is(h1, h2, h3, h4) { margin-top: 1.5em; } replaces four separate rules.

Combine :has() with Adjacent Sibling

Use :has(+ ...) as a "previous sibling" selector. For example, h2:has(+ p) selects any <h2> that is immediately followed by a <p>. This is a pattern that was impossible before :has().

CSS Counters with ::before

Use CSS counters with ::before to automatically number sections, figures, or steps without hardcoding numbers. Combined with counter-reset and counter-increment, you get self-updating numbering.

CSS
/* Auto-numbered sections */
.article {
  counter-reset: section;
}

.article h2::before {
  counter-increment: section;
  content: counter(section) ". ";
  color: #6c63ff;
  font-weight: 700;
}
Performance of :has()

While :has() is incredibly powerful, use it judiciously on very large DOM trees. Browser engines must check child elements to evaluate the parent, which is the reverse of normal selector matching. For most real-world use cases this is perfectly fast, but avoid deeply nested :has() within :has() chains.

Previous Responsive Design