CSS :has() Pseudo-Class Guide
What is :has()?
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.
Why It Matters
- 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.
Basic Syntax and Selection
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.
/* 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.
Parent Selection Based on Children
This is the most common use case — styling containers differently depending on what content they hold.
/* 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.
Sibling-Based Selection
You can use combinators inside :has() to select elements based on their siblings, not just descendants. This is where it gets really powerful.
/* 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.
Form State Management
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 |
/* 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.
Complex Conditional Styling
You can chain multiple :has() selectors or combine them with other pseudo-classes for sophisticated conditional logic.
/* 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.
Quick Cheat Sheet
| 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) |
Common Mistakes
-
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.
