Dev Encyclopedia
ArticlesToolsContactAbout

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Content

  • Articles
  • Tools
  • About
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

Legal

  • Privacy Policy
  • Terms of Service
  • Disclaimer

© 2026 Dev Encyclopedia

Back to top ↑
  1. Home
  2. /Blog
  3. /8 CSS :has() Patterns You'll Actually Use (2026)
html css9 min read

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.

Zeeshan Tofiq
Zeeshan Tofiq
May 30, 2026
On this page

On this page

  • Form Group Validation State
  • Active Nav Item
  • Content-Aware Card Layout
  • Modal Scroll Lock
  • Sibling Dimming on Hover
  • Checked Checkbox Label
  • Quantity-Based Grid Layout
  • Input Clear Button Visibility
  • Pattern Quick Reference
  • Performance Considerations
  • Frequently Asked Questions

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.

ℹ Browser support

:has() is Baseline Widely Available since 2024. Chrome, Firefox, Safari, Edge: all fully supported. Global coverage above 95% in 2026. Use it without fallbacks.

  1. 1

    Style a Form Group When Its Input Is Invalid

    The classic problem: you want the label, border, and error state on a .form-group to 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;
    }

    💡 Tip

    Combine with :has(input:user-invalid) to only show the red state after the user has interacted with the field, not on first page load.

  2. 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 — CSS
    nav li:has(a.active) {
      background-color: #ebf8ff;
      border-radius: 6px;
    }
    
    nav li:has(a.active) a {
      font-weight: 600;
      color: #2b6cb0;
    }

    💡 Tip

    With Next.js, <Link> automatically receives aria-current="page" on the active route. Use nav li:has(a[aria-current='page']) instead of a class: no extra code needed.

  3. 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;
    }

    ℹ Info

    :not(:has(img)) selects cards without images. The card decides its own layout based on what is actually inside it: no class-toggling logic needed.

  4. 4

    Lock Body Scroll When a Modal Is Open

    One line. No JavaScript. The moment a <dialog> element with the open attribute 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 — CSS
    body:has(dialog[open]) {
      overflow: hidden;
    }

    💡 Tip

    Also works with custom modals: body:has(.modal.is-open). The native <dialog> element has its open attribute managed by the browser when you call dialog.showModal() and dialog.close().

  5. 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;
    }

    ℹ Info

    Read it aloud: "when the grid has a hovered card, select every card that is not currently hovered". The parent container becomes context-aware through its children's state, with no JavaScript.

  6. 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 input
    label: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 label
    input[type="checkbox"]:checked + label {
      color: #2b6cb0;
    }

    ℹ Info

    Both patterns work for radio buttons, toggle switches, and any <input> with a :checked state. Use the :has() version when the input is inside the label; use + when they are siblings.

  7. 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;
    }

    💡 Tip

    Extend the pattern for any count: :has(:nth-child(4)) checks for at least 4 items, :has(:nth-child(n+5)) for 5 or more. Combine with @container queries for truly responsive component logic.

  8. 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-shown is 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;
    }

    ℹ Info

    :placeholder-shown is true when the placeholder is visible (field empty). :not(:placeholder-shown) targets inputs that have content. Supported in all major browsers.

Pattern Quick Reference

ProblemSelector patternTailwind v4 equivalent
Style form row on invalid input.form-group:has(input:invalid)has-[input:invalid]:border-red-500
Active nav item wrapperli: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 openbody: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 labellabel: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)) .btnN/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:

html
<!-- 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.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

Full stack developer with over 6 years of experience building production applications. Writes practical guides on JavaScript, TypeScript, React, Node.js, and cloud infrastructure. Focused on helping developers solve real problems with clean, maintainable code.

Enjoyed this article?

Get practical dev guides, tool updates, and new articles delivered straight to your inbox. No spam, unsubscribe anytime.

Related Articles

react

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.

Jun 1, 2026·10 min read
javascript

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.

Jun 1, 2026·8 min read

On this page

  • Form Group Validation State
  • Active Nav Item
  • Content-Aware Card Layout
  • Modal Scroll Lock
  • Sibling Dimming on Hover
  • Checked Checkbox Label
  • Quantity-Based Grid Layout
  • Input Clear Button Visibility
  • Pattern Quick Reference
  • Performance Considerations
  • Frequently Asked Questions
Advertisement