8 CSS :has() Patterns You'll Actually Use (2026)
CSS :has() is production-ready in every browser. Here are 8 real-world patterns: form states, sibling dimming, modal scroll-lock, and more.
On this page
For years, CSS could not style a parent based on what was inside it. If you wanted to highlight a form row when its input had an error, you needed JavaScript to add a class to the parent. That's over. :has() is in every major browser, it's production-ready, and once you see what it can do you'll wonder how you wrote CSS without it.
This post skips the syntax tour (you can read that on MDN). Instead: 8 patterns worth keeping close, each with HTML structure, copy-paste CSS, and the real-world problem it solves.
- 1
Style a Form Group When Its Input Is Invalid
The classic problem: you want the label, border, and error state on a
.form-groupto turn red when the<input>inside it fails validation. Before:has(), this required JavaScript to add a class to the parent element.html — HTML structure<div class="form-group"> <label for="email">Email</label> <input id="email" type="email" required /> <span class="error-msg">Invalid email address</span> </div>css — CSS.form-group:has(input:invalid) { border-color: #e53e3e; background-color: #fff5f5; } .form-group:has(input:invalid) label { color: #e53e3e; } .form-group:has(input:invalid) .error-msg { display: block; } - 2
Highlight a Nav Item Containing the Active Link
You have a
<li>wrapping each<a>in your navigation. You want the entire<li>to appear active. Without:has(), you'd need the active class on the<li>, not the<a>(awkward with React Router and Next.js<Link>).html — HTML structure<nav> <ul> <li><a href="/blog" class="active">Articles</a></li> <li><a href="/tools">Tools</a></li> </ul> </nav>css — CSSnav li:has(a.active) { background-color: #ebf8ff; border-radius: 6px; } nav li:has(a.active) a { font-weight: 600; color: #2b6cb0; } - 3
Cards That Adapt Layout to Their Own Content
Cards with images need a different layout than text-only cards. Previously this required JavaScript at render time or separate component variants with different class names.
html — HTML structure<!-- Card with image (gets grid layout) --> <div class="card"> <img src="..." alt="..." /> <div class="card-body">...</div> </div> <!-- Text-only card (gets centered layout) --> <div class="card"> <div class="card-body">...</div> </div>css — CSS.card { padding: 1.25rem; border-radius: 8px; border: 1px solid #e2e8f0; } .card:has(img) { display: grid; grid-template-columns: 180px 1fr; gap: 1rem; } .card:not(:has(img)) { max-width: 420px; text-align: center; } - 4
Lock Body Scroll When a Modal Is Open
One line. No JavaScript. The moment a
<dialog>element with theopenattribute exists anywhere in the page, scrolling stops. When the dialog closes, scrolling returns automatically.html — HTML structure<dialog id="my-modal"> <p>Modal content</p> <button onclick="document.getElementById('my-modal').close()">Close</button> </dialog>css — CSSbody:has(dialog[open]) { overflow: hidden; } - 5
Dim All Sibling Cards Except the Hovered One
Hover over a card in a grid and the other cards visually recede. Previously this required JavaScript mouseover event handlers on every card.
html — HTML structure<div class="grid"> <div class="card">Card 1</div> <div class="card">Card 2</div> <div class="card">Card 3</div> </div>css — CSS.grid:has(.card:hover) .card:not(:hover) { opacity: 0.5; transform: scale(0.98); transition: opacity 150ms ease, transform 150ms ease; } - 6
Style a Label When Its Checkbox Is Checked
Custom checkbox styling without JavaScript. Two HTML patterns (input nested inside label, or input and label as siblings) each has the right CSS approach:
html — HTML: input nested inside label<label> <input type="checkbox" /> Remember me </label>css — CSS: use :has() for nested inputlabel:has(input[type="checkbox"]:checked) { font-weight: 600; color: #2b6cb0; text-decoration: line-through; }html — HTML: input and label as siblings<input id="remember" type="checkbox" /> <label for="remember">Remember me</label>css — CSS: use adjacent sibling for sibling labelinput[type="checkbox"]:checked + label { color: #2b6cb0; } - 7
Different Grid Columns Based on Item Count
Three columns when there are many items, a centered single column when there are only one or two. The grid reads its own children and adjusts: quantity queries in pure CSS.
html — HTML structure<ul class="grid"> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul>css — CSS/* Three-column layout when at least 3 items exist */ .grid:has(:nth-child(3)) { grid-template-columns: repeat(3, 1fr); } /* Single centered column when fewer than 3 items */ .grid:not(:has(:nth-child(3))) { grid-template-columns: 1fr; max-width: 400px; margin: 0 auto; } - 8
Show a Clear Button Only When the Input Has Content
A text input with an inline clear button that only appears when the field contains text.
:placeholder-shownis false when the input has a value, no JavaScript needed.html — HTML structure<div class="input-wrapper"> <input type="text" placeholder="Search..." /> <button class="clear-btn" aria-label="Clear search">✕</button> </div>css — CSS.input-wrapper .clear-btn { display: none; } .input-wrapper:has(input:not(:placeholder-shown)) .clear-btn { display: flex; align-items: center; }
Pattern Quick Reference
| Problem | Selector pattern | Tailwind v4 equivalent |
|---|---|---|
| Style form row on invalid input | .form-group:has(input:invalid) | has-[input:invalid]:border-red-500 |
| Active nav item wrapper | li:has(a[aria-current='page']) | has-[a[aria-current='page']]:bg-blue-50 |
| Card layout with/without image | .card:has(img) | has-[img]:grid |
| Lock scroll when modal open | body:has(dialog[open]) | has-[dialog[open]]:overflow-hidden |
| Dim siblings on hover | .grid:has(.card:hover) .card:not(:hover) | N/A (custom CSS) |
| Checked checkbox label | label:has(input:checked) | has-[input:checked]:font-semibold |
| Grid columns by item count | .grid:has(:nth-child(3)) | N/A (custom CSS) |
| Show clear button when input filled | .wrapper:has(input:not(:placeholder-shown)) .btn | N/A (custom CSS) |
Performance Considerations
- Avoid
body:has(:hover): it recalculates styles on every mousemove across the entire page. Scope to the specific container instead. - Prefer direct child selectors (
:has(> .child)) over descendant selectors (:has(.child)) when you know the exact DOM structure. It's faster and more explicit. - Avoid deeply chained
:has()inside:has(): browsers handle it, but readability suffers quickly.
Frequently Asked Questions
Do I still need JavaScript for any of these patterns?
No: all 8 patterns in this post work in pure CSS. The modal scroll-lock, sibling dimming, and form validation styling all previously required JavaScript event listeners. :has() removes that dependency. You still need JavaScript for behavior (opening/closing a modal, submitting a form), but CSS now handles the visual response.
Can I use :has() with Tailwind CSS?
Yes. Tailwind v3.4+ added the has-* variant. For simple patterns you can write utility classes directly:
<!-- Tailwind v4 has-* variant -->
<div class="has-[input:invalid]:border-red-500 has-[input:invalid]:bg-red-50">
<input type="email" required />
</div>For complex patterns like sibling dimming or quantity queries, write custom CSS in a @layer alongside Tailwind: they work side by side.
Is there a performance cost to using :has()?
For typical UI patterns, no: performance is comparable to other complex selectors. The risk is with overly broad selectors on the document root (like body:has(:hover)), which force the browser to recalculate styles across the entire document on every match. Scope :has() to specific containers and prefer direct child selectors when possible.
How does :has() affect CSS specificity?
:has() itself adds no specificity: specificity comes from the selector inside it. .card:has(img) has the same specificity as .card img (0,1,1). .card:has(.featured) has 0,2,0 (two classes). This is lower than you might expect, which makes :has() styles easy to override.
Is :has() safe to use without a fallback?
Yes. :has() is Baseline Widely Available since 2024. Chrome, Firefox, Safari, and Edge all have full support. Global coverage is above 95% in 2026. The only browsers without support are very old Firefox versions (pre-121) that are effectively out of use. For any modern project, skip the fallback.
:has() changes what CSS can do: not by adding new visual effects, but by giving stylesheets the ability to respond to context that previously required JavaScript.
The patterns above are a starting point. Once you start reaching for :has() in daily work, you'll find new uses in almost every complex UI component you touch.
Related Articles
ARIA in React: Stop Using aria-label Wrong
Pages using ARIA average 41% more accessibility errors. Learn the correct ARIA patterns for React: icon buttons, modals, toasts, spinners, and tab panels.
npm Scripts You're Probably Not Using (But Should Be)
pre/post hooks, cross-env, npm-run-all, argument passing, and built-in variables: the npm script patterns developers Google one at a time, in one place.