CSS :has() Pseudo-Class Guide
Read on to explore css :has() pseudo-class guide — a beginner-friendly walkthrough by Codekilla.
The :has() pseudo-class is CSS's first parent selector — it lets you style an element based on what's inside it or what comes after it. Instead of only selecting children based on their parents (like .parent > .child), you can now flip the logic and select parents based on their children. Think of it as a conditional statement: "If this element has X inside it, apply these styles."
Before :has(), you needed JavaScript to achieve this kind of parent-based styling. Now it's pure CSS, and it's supported in all modern browsers (Safari 15.4+, Chrome 105+, Firefox 121+). This pseudo-class opens up powerful layout and state-driven styling patterns that were previously impossible or required hacky workarounds.
- Eliminates JavaScript dependencies — Patterns like "style a card differently if it contains an image" no longer need DOM manipulation or class-toggling scripts.
- Conditional layouts — Build responsive components that adapt based on content, not just viewport size.
- Form validation UX — Style form containers based on input states (
:invalid,:checked) without touching the input elements themselves. - Cleaner component architecture — Parent components can respond to child states, making CSS more powerful and reducing the need for utility classes.
- Performance boost — Browser-native selection is faster than JavaScript observers watching for DOM changes.
The :has() pseudo-class takes a relative selector as its argument. The element you're selecting is the one that "has" the specified descendant or sibling.
css/* Select any <article> that contains an <img> */ article:has(img) { border: 2px solid blue; background: #f0f8ff; } /* Select form that has an invalid input */ form:has(input:invalid) { border-left: 4px solid red; } /* Select card that has both heading and image */ .card:has(h2):has(img) { display: grid; grid-template-columns: 1fr 2fr; }
You're not styling the img or input — you're styling their ancestors based on their presence or state. The selector inside :has() can be any valid CSS selector, including pseudo-classes, attribute selectors, and combinators.
This is the most common use case — styling containers differently depending on what content they hold.
css/* Card with featured image gets special treatment */ .card:has(.featured-image) { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; } /* List items containing links get hover effects */ li:has(a) { padding-left: 1rem; border-left: 3px solid transparent; transition: border-color 0.2s; } li:has(a):hover { border-left-color: #667eea; background: #f7f7f7; } /* Section with no content gets hidden */ section:not(:has(*)) { display: none; }
Notice the last example uses :not(:has(*)) to target empty sections. This pattern is perfect for dynamic content where some containers might be empty based on user data or API responses.
You can use combinators inside :has() to select elements based on their siblings, not just descendants. This is where it gets really powerful.
css/* Style a label when its following input is focused */ label:has(+ input:focus) { color: #667eea; font-weight: 600; } /* Heading followed by an image */ h2:has(+ img) { margin-bottom: 0.5rem; /* Less space before image */ } /* Figure that has a figcaption */ figure:has(figcaption) { border: 1px solid #ddd; padding: 1rem; }
The + combinator means "immediately following sibling." You can also use ~ for any following sibling. This lets you create contextual styles based on document structure without adding classes.
Forms are where :has() truly shines. You can style form containers, fieldsets, or sections based on input states.
| State Pattern | Use Case |
|---|---|
form:has(input:invalid) | Show error border on entire form |
fieldset:has(input:checked) | Highlight selected option groups |
div:has(input:focus) | Style wrapper when input is active |
.form-group:has(input:required:invalid) | Show validation feedback on field groups |
css/* Form section with errors */ .form-section:has(input:invalid:not(:placeholder-shown)) { background: #fff5f5; border-left: 4px solid #e53e3e; padding-left: 1rem; } /* Checkbox group with checked items */ .checkbox-group:has(input:checked) { background: #f0fff4; border-color: #38a169; } /* File input wrapper when file is selected */ .file-upload:has(input[type="file"]:valid) { border-color: #38a169; } .file-upload:has(input[type="file"]:valid)::after { content: "✓ File selected"; color: #38a169; font-size: 0.875rem; }
The :not(:placeholder-shown) trick ensures you only show errors after the user has started typing, not on page load.
You can chain multiple :has() selectors or combine them with other pseudo-classes for sophisticated conditional logic.
css/* Article with image AND video */ article:has(img):has(video) { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; } /* Container with more than 5 children (using :nth-child) */ .grid:has(:nth-child(6)) { grid-template-columns: repeat(3, 1fr); } /* Dark mode toggle affecting entire page */ body:has(#dark-mode:checked) { background: #1a202c; color: #e2e8f0; } body:has(#dark-mode:checked) a { color: #63b3ed; }
The dark mode example shows how a single checkbox can control the entire page's theme without JavaScript — just use :has() on the body or :root element.
| Need | Reach for |
|---|---|
| Style parent based on child | parent:has(child) |
| Style based on sibling | element:has(+ sibling) |
| Form validation styling | form:has(input:invalid) |
| Conditional layouts | container:has(.specific-class) |
| Empty state handling | element:not(:has(*)) |
| Multiple conditions | el:has(.a):has(.b) |
| Checked checkbox parent | label:has(input:checked) |
-
Forgetting browser support — While modern browsers support
:has(), always check@supportsfor critical layouts or provide fallbacks for older browsers. -
Overusing :has() for simple descendant selectors — If you're just styling children, use normal descendant selectors. Don't write
.parent:has(.child) .childwhen.parent .childworks fine. -
Performance concerns with deeply nested :has() — Avoid chaining too many
:has()selectors or using them with universal selectors like:has(*)on large DOMs — the browser has to do more work. -
Not considering empty states — When using
:has()for layout changes, always test what happens when elements are empty or missing. Use:not(:has(*))to handle empty containers. -
Forgetting specificity rules —
:has()has the same specificity as a class selector, but the selectors inside it also count.div:has(.foo)has higher specificity than justdiv. -
Using :has() with pseudo-elements — You can't select pseudo-elements with
:has(). This won't work:div:has(::before). Pseudo-elements aren't in the DOM.
💡 Think Like a Programmer: The
:has()pseudo-class is CSS's answer to conditional rendering — instead of writing "if this, then that" in JavaScript, you're declaring "when this exists, style that" directly in your stylesheets. It's declarative logic for visual states.
Keep Reading
Complete Guide to CSS Variables with Examples
Read on to explore complete guide to css variables with examples — a beginner-friendly walkthrough by Codekilla.
CSS Selectors Explained — Complete Guide
Read on to explore css selectors explained — complete guide — a beginner-friendly walkthrough by Codekilla.
All CSS Properties Cheat Sheet with Examples
Read on to explore all css properties cheat sheet with examples — a beginner-friendly walkthrough by Codekilla.
