Dev Encyclopedia
ArticlesTools
ArticlesTools

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
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

© 2026 Dev Encyclopedia

Privacy PolicyTermsDisclaimer
  1. Home
  2. /Blog
  3. /8 CSS :has() Patterns You'll Actually Use (2026)
html css8 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.

By Dev EncyclopediaPublished May 30, 2026
On this page

On this page

  • Introduction
  • 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
  • When Not to Use :has()
  • Frequently Asked Questions

Introduction

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 copy-paste CSS and the real-world problem it solves.

Step-by-Step Guide

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.

css
.form-group:has(input:invalid) {
  border-color: #e53e3e;
  background-color: #fff5f5;
}

.form-group:has(input:invalid) label {
  color: #e53e3e;
}

💡 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

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> — which is awkward with React Router and Next.js <Link>.

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, the <Link> component automatically receives an aria-current="page" attribute on the active route. Use nav li:has(a[aria-current='page']) instead of a class.

3

Cards That Adapt Layout to Their Own Content

Cards with images need a different layout than text-only cards. Previously this meant JavaScript at render time or separate component variants with different class names.

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. This is quantity-agnostic — the card decides its own layout based on what is actually inside it.

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.

css
body:has(dialog[open]) {
  overflow: hidden;
}

💡 Tip

This also works with any custom modal: body:has(.modal.is-open). If you use the native <dialog> element, the open attribute is added and removed by the browser automatically when you call dialog.showModal() and dialog.close().

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.

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

Style a Label When Its Checkbox Is Checked

Custom checkbox styling without JavaScript. The label containing or adjacent to a checked checkbox styles itself directly:

  • The :has() version works when the <input> is nested inside the <label> element.
  • The + adjacent sibling version works when <input> and <label> are siblings — no :has() needed for that specific pattern.
  • Both work for radio buttons, toggle switches, and any <input> with a :checked state.
css
label:has(input[type="checkbox"]:checked) {
  font-weight: 600;
  color: #2b6cb0;
  text-decoration: line-through;
}

/* For adjacent label pattern */
input[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.

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)) checks for 5 or more. Combine with @container queries for truly responsive component logic.

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.

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 (i.e., the field is empty). Negating it with :not() targets inputs that have content. Supported in all major browsers.

9

When Not to Use :has()

:has() is production-ready, but a few patterns create unnecessary style recalculations. The rule: keep selectors scoped.

  • 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.

ℹ Info

Browser support: all major browsers have supported :has() since late 2023. Global coverage is above 95% in 2026. Use it without fallbacks for any modern project.

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, so you can write has-[input:invalid]:border-red-500 directly in your class list. If you need more complex :has() patterns, write them in a custom CSS file or inside a @layer — Tailwind and custom CSS 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 — the specificity comes from the selector inside it. So .card:has(img) has the same specificity as .card img (one class + one tag = 0,1,1). :has() with a class inside — .card:has(.featured) — has specificity 0,2,0 (two classes). This is lower than you might expect, which makes it easy to override.
Is :has() safe to use without a fallback?
Yes. :has() is a Baseline Widely Available feature as of 2024. All major browsers — Chrome, Firefox, Safari, Edge — have full support. Global coverage is above 95% in 2026. The only browsers without support are very old versions of Firefox (pre-121) that are effectively out of use. For any modern project, use it without fallbacks.

: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

nextjs

How to Use Environment Variables in Next.js (Without Leaking Them to the Browser)

Learn how to use .env files in Next.js correctly. Understand NEXT_PUBLIC_, avoid common mistakes, and set variables in Vercel and Cloudflare.

May 30, 2026·7 min read
javascript

5 async/await Mistakes That Slow Your JavaScript Code

Sequential awaits, await in forEach, missing Promise.all — these 5 async/await mistakes silently slow your JavaScript. Here's how to spot and fix each one.

May 30, 2026·8 min read

On this page

  • Introduction
  • 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
  • When Not to Use :has()
  • Frequently Asked Questions