CSS :has() Selector: The Parent Selector That Changed Everything - By Sourav Mishra (@souravvmishra)
Learn CSS :has() parent selector with 10+ real-world examples. Style parents based on children, handle form states, conditional layouts, and more.
For years, CSS developers asked for one thing: a parent selector. We wanted to style a parent based on its children. And for years, the answer was "it's impossible" or "use JavaScript."
Then :has() arrived, and everything changed.
What is :has()?
The :has() selector lets you select an element based on its descendants or siblings. It's like asking: "Does this element have a child that matches this selector?"
/* Select any card that contains an image */
.card:has(img) {
padding: 0;
}
/* Select a label when its input is focused */
label:has(+ input:focus) {
color: blue;
}
This was literally impossible before without JavaScript.
Real-World Use Cases
1. Form Field Styling
Style a form group based on input state:
/* Highlight the entire field group when input is focused */
.form-group:has(input:focus) {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Show error styling on the group when input is invalid */
.form-group:has(input:invalid) {
border-color: #ef4444;
}
/* Dim the group when input is disabled */
.form-group:has(input:disabled) {
opacity: 0.5;
pointer-events: none;
}
2. Card Layouts
Adjust card styles based on content:
/* Cards with images get no top padding */
.card:has(> img:first-child) {
padding-top: 0;
}
/* Cards with videos get a play button overlay */
.card:has(video)::before {
content: "▶";
position: absolute;
/* ... play button styles */
}
/* Feature cards (with .featured class child) get highlighted */
.card:has(.featured) {
border: 2px solid gold;
}
3. Navigation Active States
Style navigation based on current page:
/* Highlight nav item when it contains the current page link */
nav li:has(a[aria-current="page"]) {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
/* Style the entire header when mobile menu is open */
header:has(#mobile-menu:checked) {
background: #000;
position: fixed;
inset: 0;
}
4. Conditional Grid Layouts
Change layout based on content count:
/* Single item: center it */
.grid:has(> :only-child) {
display: flex;
justify-content: center;
}
/* Two items: side by side */
.grid:has(> :nth-child(2):last-child) {
grid-template-columns: 1fr 1fr;
}
/* Three or more: standard grid */
.grid:has(> :nth-child(3)) {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
5. Empty State Handling
Show placeholders when content is empty:
/* Show empty state when list has no items */
.todo-list:not(:has(li))::after {
content: "No tasks yet. Add one above!";
color: #666;
font-style: italic;
}
/* Or when all items are completed */
.todo-list:has(li:checked):not(:has(li:not(:checked)))::after {
content: "All done! 🎉";
}
6. Table Row Highlighting
Style rows based on cell content:
/* Highlight rows with high priority */
tr:has(td.priority-high) {
background: #fef2f2;
border-left: 3px solid #ef4444;
}
/* Fade completed rows */
tr:has(input[type="checkbox"]:checked) {
opacity: 0.5;
}
Combining :has() with :not()
The combination of :has() and :not() is incredibly powerful:
/* Style articles WITHOUT images differently */
article:not(:has(img)) {
padding-left: 2rem;
border-left: 4px solid #e5e7eb;
}
/* Form is valid only when ALL required fields are filled */
form:not(:has(input:required:invalid)) button[type="submit"] {
background: #22c55e;
cursor: pointer;
}
form:has(input:required:invalid) button[type="submit"] {
background: #9ca3af;
cursor: not-allowed;
}
Performance Considerations
:has() evaluates from the subject to the argument, which can be expensive. A few tips:
/* ❌ Avoid: Checking entire document */
body:has(.dialog-open) {
overflow: hidden;
}
/* ✅ Better: Scope to a specific container */
.app:has(.dialog-open) {
overflow: hidden;
}
/* ❌ Avoid: Deep descendant checks */
div:has(span a img) { ... }
/* ✅ Better: Direct child when possible */
div:has(> img) { ... }
Modern browsers are highly optimized, but keeping selectors simple helps.
Browser Support
As of late 2025, :has() is supported in all major browsers:
- Chrome 105+
- Firefox 121+
- Safari 15.4+
- Edge 105+
For older browser fallbacks:
/* Fallback styles */
.card {
padding: 1rem;
}
/* Enhanced styles with :has() */
@supports selector(:has(*)) {
.card:has(img) {
padding: 0;
}
}
Key Takeaways
:has()is the parent selector we've wanted for decades- Use it for state-based styling - focus, hover, checked, valid/invalid
- Combine with
:not()for inverse selections - Keep selectors simple for best performance
- Check browser support if you need to support older browsers
The CSS :has() selector eliminates the need for JavaScript in so many styling scenarios. It's not just a quality-of-life improvement - it's a paradigm shift in how we think about CSS.
Start using it today. Your future self will thank you.
Building modern web apps? Check out my guides on Next.js Server Actions and why I recommend shadcn for component libraries.
Frequently Asked Questions
Q: What is the CSS :has() selector?
The CSS :has() selector is a parent selector that lets you style an element based on its descendants or siblings. For example, .card:has(img) selects cards that contain images. It was previously impossible without JavaScript.
Q: Is CSS :has() supported in all browsers?
Yes, as of 2025, :has() is supported in all major browsers: Chrome 105+, Firefox 121+, Safari 15.4+, and Edge 105+. For older browsers, use @supports selector(:has(*)) for fallbacks.
Q: Does :has() have performance issues?
Modern browsers are highly optimized for :has(). However, avoid very broad selectors like body:has(.class) or deep descendant checks. Scope selectors to specific containers and prefer direct child checks (>) when possible.
Q: Can I use :has() with :not()?
Yes, combining :has() and :not() is powerful. For example, article:not(:has(img)) selects articles without images. This enables conditional styling based on presence or absence of child elements.