Skip to main content
🎨 CSS as a design system·Module B8 · Lesson 3
TaskBuild a card system where 'featured' status is detected via :has. .card { padding: 16px; border: 1px solid #e5e7eb; border-radius: 8px; }. .card:has(.featured-badge) { border-color: #F59E0B; box-shadow: 0 0 0 3px rgba(245,158,11,0.1); }. Two cards, one with <span class='featured-badge'>★</span>.

:has() — the parent selector finally arrives

100 XP7 min
Theory

The selector the web waited 20 years for

.card:has(img) { padding-top: 0; }                /* cards that CONTAIN an image */
.card:has(.featured) { border: 2px solid gold; }  /* cards with a .featured child */
form:has(input:invalid) { border-color: red; }    /* form with any invalid input */

:has(selector) matches the OUTER element if the inner selector matches anywhere inside. Finally — a parent selector in CSS.

The killer use cases

Style parents based on children

li:has(> input[type="checkbox"]:checked) {
  text-decoration: line-through;
  opacity: 0.6;
}

Strikethrough a todo list item when its checkbox is checked. Pure CSS, no JS.

Layout shifts based on content

.layout:has(aside) { grid-template-columns: 1fr 240px; }
.layout:not(:has(aside)) { grid-template-columns: 1fr; }

Layout shifts based on whether a sidebar exists.

Form validation styling

form:has(:invalid) button[type="submit"] {
  background: #ccc;
  cursor: not-allowed;
}

Submit button disables visually when ANY input in the form is invalid.

Browser support

Shipped 2022-2023 in all evergreens. Safe in 2026.

Performance

:has does extra work — browsers optimize aggressively but avoid in HOT loops (every scroll frame). For static layout selectors, fine.

🔒

Sign up to start coding

Theory is open to everyone. The interactive editor, live preview, and check are unlocked with a 7-day free trial — card required, cancel anytime.

Sign up — free trial →

First 10 lessons in each track are free. No card needed for those.

PreviousNext lesson →

Get one Python or web tip a day — by email

Short, hand-written, no spam. Unsubscribe in one click.