Explore how CSS :has() unlocks parent-level styling and smarter UI design with just pure CSS, no JavaScript needed!
CSS has long been evolving to make styling more powerful and intuitive. One of the newest
and most exciting additions to the CSS selector arsenal is the
:has()
pseudo-class. This selector enables parent or ancestor selection based
on child elements, something that was previously impossible in pure CSS until December 2023.
In this article, we'll explore everything you need to know about :has()
:
what it is, how it works, its practical uses, browser compatibility, and some gotchas to
watch out for.
What is the :has() Pseudo-Class?
The :has()
pseudo-class selector allows you to select an element
if it contains at least one element that matches the selector inside the
parentheses.
In other words, it's a parent selector that matches an element based on the presence of a child or descendant.
Syntax
E:has(s) {
/* Rules applied to E who has s */
}
E
= the element you want to style conditionally-
s
= a selector that matches child or descendant elements insideE
Why is :has() Special?
For decades, CSS didn't had a way to style an element based on its children or descendants. You could style children based on parents, but the reverse was impossible without JavaScript.
With :has()
, you get this ability natively in CSS, enabling more dynamic and
context-aware styling, purely declarative and performant.
Why Use :has() Instead of JavaScript?
- Performance: Browser optimizes CSS selectors better than DOM manipulation scripts.
- Simplicity: Less code, fewer event listeners.
- Maintainability: Declarative styling, easier to understand.
- Progressive Enhancement: Works gracefully on modern browsers, doesn't break UI if unsupported.
Try It Yourself!
Here's a simple example you can test in a modern browser console or JSFiddle:
<style>
div:has(p.highlight) {
border: 2px solid green;
padding: 10px;
}
p.highlight {
color: red;
}
</style>
<div>
<p>This paragraph is normal.</p>
</div>
<div>
<p class="highlight">This paragraph is highlighted.</p>
</div>
Only the second <div>
will get the green border because it contains a
<p>
with the .highlight
class.
How :has() Works
Example 1: Highlight a Section If It Contains a Focused Input
Imagine a form with multiple input fields inside different sections. You want to highlight the section when an input inside it receives focus.
section:has(input:focus) {
border: 2px solid blue;
background-color: #f0f8ff;
}
This CSS selects any <section>
element that contains an
<input>
currently focused, highlighting that entire section.
Example 2: Style a List Item That Contains a Checkbox That's Checked
You can style parent elements based on child states, useful for custom UI effects.
li:has(input[type="checkbox"]:checked) {
background-color: #d4edda;
font-weight: bold;
}
This styles any <li>
that contains a checked checkbox.
Example 3: Show a Warning if a Form Has an Invalid Input
Highlight the form itself if it contains invalid inputs:
form:has(input:invalid) {
border: 2px solid red;
}
Practical & Useful Examples of :has()
1. Highlight a Menu Item When Its Dropdown Is Open
If you have a navigation menu where hovering or focusing on a submenu should highlight the parent menu item:
nav li:has(ul:hover),
nav li:has(ul:focus-within) {
background-color: #eee;
font-weight: bold;
}
Why useful?
You don't need JS to highlight the parent
<li>
when the submenu <ul>
is hovered or focused.
2. Style a Card Differently If It Contains an Image
If you want cards with images to have a different background than those without:
.card:has(img) {
background-color: #f9f9f9;
border: 1px solid #ddd;
}
Cards without images stay default; cards with images get a subtle background and border.
3. Disable a Submit Button If a Form Contains Invalid Inputs
You can visually disable or style the submit button based on whether the form contains invalid inputs:
form:has(input:invalid) button[type="submit"] {
background-color: #ccc;
cursor: not-allowed;
pointer-events: none;
}
This gives instant visual feedback without JS!
4. Style a Parent Container If It Contains a Checked Checkbox
Checkboxes inside a list that, when checked, style the entire list item:
li:has(input[type="checkbox"]:checked) {
background-color: #d1e7dd;
text-decoration: line-through;
color: #495057;
}
Great for todo lists, task managers, etc.
5. Show a Warning Message Only If a Form Has Required Empty Fields
If you have a warning element inside a form, show it only when required inputs are empty:
form:has(input:required:invalid) .warning-message {
display: block;
color: red;
}
.warning-message {
display: none;
}
This CSS-only validation UI enhancement reduces reliance on JS.
6. Expand an Accordion Section When It Contains a Focused Element
If an accordion expands when any of its children (like inputs or buttons) gain focus:
.accordion-item {
overflow: hidden;
}
.accordion-item .accordion-content {
max-height: 0;
transition: max-height 0.3s ease;
}
.accordion-item:has(:focus-within) .accordion-content {
max-height: 500px; /* expand */
}
Note: This assumes your accordion content has a reasonable maximum height.
For truly dynamic content, consider using max-height: none
with a transition on
height
instead.
7. Prevent Body Scroll When Sidebar Is Open
Control the entire page's overflow behavior based on whether a sidebar or modal is currently open:
body:has(.sidebar-open) {
overflow: hidden;
}
Why useful?
When you have a mobile sidebar or modal overlay, you
typically want to prevent the background page from scrolling. Instead of toggling classes
with JavaScript on the body element, you can simply add/remove the
.sidebar-open
class on the sidebar itself, and CSS handles the rest.
HTML structure:
<body>
<div class="sidebar" id="mySidebar">
<!-- sidebar content -->
</div>
<main>
<!-- page content -->
</main>
</body>
JavaScript (minimal):
// Just toggle the class on the sidebar
document.getElementById("mySidebar").classList.toggle("sidebar-open")
// CSS automatically handles body overflow
This approach keeps your JavaScript focused on state management while letting CSS handle the styling consequences.
8. Dark Mode Toggle Without JavaScript State Management
Let a single checkbox control your entire page's theme without complex JavaScript state management:
body:has(.theme-toggle:checked) {
background-color: #1a1a1a;
color: #ffffff;
}
body:has(.theme-toggle:checked) .card {
background-color: #2d2d2d;
border-color: #444;
}
body:has(.theme-toggle:checked) .btn {
background-color: #495057;
border-color: #6c757d;
}
Why useful?
Traditional dark mode implementations require JavaScript
to toggle classes on the body or root element. With :has()
, you just need a
checkbox anywhere in your DOM, and CSS handles all the theme switching logic declaratively.
9. E-commerce Product Cards with Dynamic States
Style product cards differently based on their current state (sale, out of stock, etc.):
.product-card:has(.sale-price) .original-price {
text-decoration: line-through;
color: #999;
font-size: 0.9em;
}
.product-card:has(.sale-price) {
border: 2px solid #dc3545;
position: relative;
}
.product-card:has(.sale-price)::before {
content: "SALE";
position: absolute;
top: 10px;
right: 10px;
background: #dc3545;
color: white;
padding: 2px 8px;
font-size: 0.8em;
border-radius: 3px;
}
.product-card:has(.out-of-stock) {
opacity: 0.6;
pointer-events: none;
}
Why useful?
No need to manually add modifier classes like
.product-card--on-sale
. Just add the sale price or stock status elements, and
CSS automatically styles the entire card accordingly.
10. Form Step Indicators with Validation States
Create visual step indicators that automatically update based on form validation:
.form-step:has(input:invalid) .step-indicator {
background-color: #dc3545;
color: white;
}
.form-step:has(input:valid) .step-indicator {
background-color: #28a745;
color: white;
}
.form-step:has(input:focus) .step-indicator {
border: 2px solid #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
.form-step:has(input:placeholder-shown) .step-indicator {
background-color: #6c757d;
}
Why useful?
Perfect for multi-step forms or wizards. The step
indicators automatically reflect the current validation state without needing JavaScript to
manually update their appearance.
11. Media Gallery with Selection Mode
Reveal batch actions and visual feedback when media items are selected:
.gallery:has(.media-item.selected) .batch-actions {
display: flex;
position: sticky;
bottom: 20px;
background: #007bff;
color: white;
padding: 1rem;
border-radius: 8px;
margin: 1rem;
}
.gallery:has(.media-item.selected) .media-item:not(.selected) {
opacity: 0.5;
transform: scale(0.95);
}
.batch-actions {
display: none;
transition: all 0.3s ease;
}
Why useful?
Common pattern in admin interfaces, photo galleries, or
file managers. Once any item is selected, the interface automatically switches to
"selection mode" with batch actions and visual feedback.
12. Comment System with Interactive States
Style comment threads based on their current interaction state:
.comment:has(.reply-form:not([hidden])) {
border-left: 3px solid #007bff;
background-color: #f8f9fa;
padding-left: 1rem;
}
.comment:has(.replies) {
margin-bottom: 1.5rem;
}
.comment:has(.replies)::after {
content: "";
display: block;
width: 2px;
background: #dee2e6;
position: absolute;
left: 20px;
height: 100%;
}
Why useful?
Automatically style comments when reply forms are open or
when they contain reply threads, creating a more dynamic and responsive comment interface.
13. Navigation with Notification Badges
Show notification indicators only when there are actual notifications:
.nav-item:has(.notification-count:not(:empty)) .nav-link::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 8px;
background-color: #dc3545;
border-radius: 50%;
}
.nav-item:has(.notification-count:not(:empty)) .nav-link {
position: relative;
}
/* Show actual count for numbers */
.nav-item:has(.notification-count[data-count]) .nav-link::after {
content: attr(data-count);
width: auto;
height: auto;
padding: 2px 6px;
font-size: 0.7em;
line-height: 1;
min-width: 16px;
text-align: center;
}
Why useful?
Clean, semantic approach to notification badges. The badge
only appears when there's actual content, and you can even display the count using data
attributes.
Bonus: Nested Example for Complex UI Logic
Highlight a comment thread if any reply inside is authored by the current user:
.comment-thread:has(.reply.author-current-user) {
border-left: 4px solid #007bff;
background-color: #e7f1ff;
}
Use Cases & Benefits
- Parent selection without JavaScript: No more need for JS hacks or libraries just to style parents based on children.
- Better form styling: Style form groups or error containers dynamically.
- Conditional UI states: Trigger complex UI changes based on child elements.
- Cleaner code: Reduce reliance on JS for simple conditional styling.
Browser Support
The :has()
pseudo-class was standardized in
Selectors Level 4 and is
supported by most modern browsers:
Browser | Support |
---|---|
Chrome | Supported (from v105) |
Edge | Supported (from v105) |
Firefox | Supported (from v121) |
Safari | Supported (from v15.4) |
Opera | Supported (from v91) |
Internet Explorer | Not supported |
Note: Always check up-to-date compatibility on Can I Use before using in production.
Performance Considerations
Because :has()
lets you select parents based on child selectors, browsers need
to do more complex matching. This can have performance implications if used on very large
DOM trees or with very complex selectors.
What counts as "complex selectors":
- Deep descendant selectors like
:has(.level1 .level2 .level3 .target)
- Multiple pseudo-classes like
:has(input:focus:valid:required)
- Universal selectors like
:has(*:hover)
- Combining
:has()
with other expensive selectors
Performance tips:
- Use
:has()
selectively and avoid overly broad selectors - Prefer child selectors (
>
) over descendant selectors when possible - Avoid using it inside animations or frequently triggered styles
- Test performance if applying on large or complex pages
-
Consider scoping your
:has()
selectors to specific containers rather than applying globally
Gotchas and Limitations
- Browser support: Not supported on IE or older browsers, fallback or polyfills may be needed
-
Performance impact: Complex selectors inside
:has()
may reduce performance on large DOMs -
Sibling relationships: You can select based on sibling state using
combinators like
:has(+ .next-sibling)
or:has(~ .any-following-sibling)
-
Specificity:
:has()
itself has no specificity, but the selectors inside it count toward specificity calculations -
Cannot nest
:has()
: You cannot use:has()
inside another:has()
selector
Summary
The CSS :has()
pseudo-class selector is a powerful new tool that finally
unlocks parent selection based on children, something developers have long needed. It
simplifies conditional styling, reduces dependency on JavaScript, and opens doors for
cleaner, more maintainable CSS.
While browser support is solid and growing, always check compatibility and performance
before using it in production. When used thoughtfully, :has()
can eliminate
many JavaScript-based styling solutions and make your CSS more declarative and easier to
maintain.