1. :has() - The Parent Selector

:has() selects an element based on what it contains. It's the long-awaited CSS parent selector. Style a form based on its input state, a card when its checkbox is checked, and more.

Form validation with :has()

Type a valid or invalid email. The form border changes color using :has(:invalid).

Checkbox card selection

Cards highlight when their checkbox is checked. Pure CSS with :has(:checked).

/* Style parent based on child state */
.form:has(input:invalid) {
  border-color: red;
  background: #fef2f2;
}

.form:has(input:valid) {
  border-color: green;
  background: #f0fdf4;
}

/* Card highlights when checkbox inside is checked */
.card:has(input:checked) {
  border-color: #6366f1;
  background: #eef2ff;
}

/* Show sibling when input has content */
.field:has(input:not(:placeholder-shown)) .hint {
  display: block;
}

/* Style based on child element presence */
.card:has(img)  { /* card with an image */ }
.card:has(> h2) { /* card with a direct h2 child */ }

2. :is() and :where()

Both let you group selectors to avoid repetition. The key difference: :is() takes the highest specificity of its arguments, while :where() always has zero specificity.

:is() - takes highest specificity

Heading 3

Heading 4

Heading 5

All headings styled indigo via :is(h3, h4, h5)

Specificity = highest selector in the list (e.g., same as h3).

:where() - zero specificity

Heading 3

Heading 4

Heading 5

Same styling via :where(h3, h4, h5), easy to override

Specificity = 0. Perfect for default/reset styles that are easy to override.

/* Without :is() - repetitive */
article h1, article h2, article h3,
section h1, section h2, section h3 {
  color: navy;
}

/* With :is() - clean and concise */
:is(article, section) :is(h1, h2, h3) {
  color: navy;
}

/* :where() for low-specificity defaults */
:where(h1, h2, h3) { color: inherit; }

/* Key difference: specificity */
:is(#id, .class) p  { }  /* specificity: (1,0,1), takes #id */
:where(#id, .class) p { } /* specificity: (0,0,1), always zero */

/* Great for CSS resets */
:where(ul, ol) { list-style: none; padding: 0; }

3. :not() - Negation

:not() excludes elements matching the given selector. Modern CSS supports complex selectors and comma-separated lists inside :not().

Exclude disabled items
  • Active Item 1
  • Disabled Item
  • Active Item 2
  • Disabled Item
  • Active Item 3

Only non-disabled items get hover effects via li:not(.disabled):hover.

Border except last
  • Item with border
  • Item with border
  • Item with border
  • Last item, no border

li:not(:last-child) adds borders to all but the last item.

/* Exclude specific class */
li:not(.disabled):hover { background: #eef2ff; }

/* All but last child */
li:not(:last-child) { border-bottom: 1px solid #e5e7eb; }

/* Multiple exclusions (modern CSS) */
input:not([type="submit"], [type="reset"]) {
  border: 2px solid #e5e7eb;
}

/* Complex selectors */
.card:not(:has(img)) { min-height: 10rem; }

/* Exclude first and last */
li:not(:first-child):not(:last-child) {
  padding: 1rem;
}

4. Attribute Selectors

Target elements by their attributes using pattern-matching operators: starts-with (^=), ends-with ($=), contains (*=), and more.

/* Attribute presence */
[data-tooltip] { cursor: help; }

/* Exact match */
[type="email"] { border-color: blue; }

/* Starts with */
a[href^="https"]  { color: green; }   /* external links */
a[href^="mailto"] { color: blue; }    /* email links */
a[href^="#"]      { color: purple; }   /* anchor links */

/* Ends with */
a[href$=".pdf"]  { color: red; }      /* PDF links */
a[href$=".zip"]  { color: orange; }   /* ZIP downloads */

/* Contains */
a[href*="example"] { font-weight: bold; }

/* Custom data attributes */
[data-theme="dark"] { background: #1e293b; }
[data-size="lg"]    { font-size: 1.25rem; }

/* Case-insensitive flag */
a[href$=".PDF" i] { color: red; }  /* matches .pdf, .PDF, .Pdf */

5. Combinators

Combinators define the relationship between selectors: descendant (space), child (>), adjacent sibling (+), and general sibling (~).

Child vs Descendant
parent > child (direct only)
Parent
Direct child (styled)
Nested grandchild (not styled)

> only targets direct children, not deeper descendants.

Adjacent + General siblings
A + B (adjacent) and A ~ B (general)
H2
Target
H2 + p
(adjacent)
H2 ~ p
(general)
H2 ~ p
(general)

+ = next sibling only. ~ = all following siblings.

/* Descendant (space): any depth */
article p { line-height: 1.6; }

/* Child (>): direct children only */
.nav > li { display: inline-block; }

/* Adjacent sibling (+): next sibling only */
h2 + p { margin-top: 0; }       /* first paragraph after heading */
input + .error { color: red; }    /* error right after input */

/* General sibling (~): all following siblings */
h2 ~ p { color: #4b5563; }       /* all paragraphs after heading */
input:invalid ~ .hint { display: block; }

/* Practical examples */
.checkbox:checked + label { font-weight: bold; }
img + figcaption { font-style: italic; }
details[open] > summary { color: blue; }

6. :nth-child() Patterns

Select elements by position using keywords (odd, even) or the An+B formula. The of S syntax adds type-filtering.

:nth-child(odd)
1
2
3
4
5
6
7
8
9
10

Highlights 1st, 3rd, 5th, 7th, 9th items.

:nth-child(3n)
1
2
3
4
5
6
7
8
9
10

Every 3rd item: 3, 6, 9.

:nth-child(-n+3)
1
2
3
4
5
6
7
8
9
10

First 3 items only. -n+3 counts down: 3, 2, 1.

:nth-last-child(-n+2)
1
2
3
4
5
6
7
8
9
10

Last 2 items. :nth-last-child counts from the end.

/* Keywords */
tr:nth-child(odd)  { background: #f9fafb; }  /* zebra stripes */
tr:nth-child(even) { background: white; }

/* An+B formula */
li:nth-child(3n)      { }  /* every 3rd: 3, 6, 9... */
li:nth-child(3n+1)    { }  /* 1st of every group of 3: 1, 4, 7... */
li:nth-child(-n+3)    { }  /* first 3 only: 3, 2, 1 */
li:nth-child(n+4)     { }  /* from 4th onward: 4, 5, 6... */

/* From the end */
li:nth-last-child(-n+2) { }  /* last 2 items */
li:nth-last-child(1)    { }  /* last item (same as :last-child) */

/* "of S" syntax (newest) */
li:nth-child(2 of .highlight) { }  /* 2nd highlighted item */
p:nth-child(odd of :not(.hidden)) { }  /* odd among visible paragraphs */

7. :focus-visible vs :focus

:focus-visible only shows focus styles when the user navigates via keyboard, not on mouse click. Better UX than :focus for buttons and links.

Click vs Tab: try both

Click each button (no outline on :focus-visible), then Tab to them (outline appears). :focus always shows outline.

/* :focus - always shows (mouse + keyboard) */
button:focus {
  outline: 3px solid #6366f1;
  outline-offset: 2px;
}

/* :focus-visible - only keyboard navigation */
button:focus-visible {
  outline: 3px solid #6366f1;
  outline-offset: 2px;
}

/* Best practice: remove default, add focus-visible */
button:focus {
  outline: none;
}
button:focus-visible {
  outline: 3px solid #6366f1;
  outline-offset: 2px;
}

/* Combined: subtle focus + strong focus-visible */
input:focus {
  border-color: #6366f1;
}
input:focus-visible {
  outline: 3px solid #6366f1;
  outline-offset: 2px;
}