CSSAWWWARDS
+ Submit tool
toolsIntermediate · Updated July 2026

CSS :has() Selector: The Complete Parent Selector Guide (2026)

The CSS :has() selector is what developers requested for two decades: a way to style a parent based on its children. It is now fully supported in every major browser, replacing dozens of JavaScript class-toggle patterns with a single CSS declaration.

By Adil Badshah21 July 202613 min read
CSS :has() Selector Complete Parent Selector Guide 2026

What Is the CSS :has() Selector?

Quick Answer

:has() is a CSS relational pseudo-class that matches an element if the selector argument matches at least one element reachable from it. In practice it works as the long-awaited “parent selector”: .card:has(img) styles .card elements that contain an img. It has full browser support since late 2023 and is production-ready.

“How do I select a parent element in CSS?” was one of the most common CSS questions on Stack Overflow for over a decade. The answer for years was: you can’t — at least not without JavaScript. CSS selectors have always been able to look forward and downward in the DOM (descendants, children, adjacent siblings) but never upward or backward. :has() changes this fundamental constraint.

The specification positions :has()as a “relational pseudo-class” rather than strictly a “parent selector” because it can match elements based on descendants at any level, not just immediate parents. But its most common use case is exactly that: style an ancestor based on what it contains.

Safari shipped support for :has() in Safari 15.4 (March 2022). Chrome and Edge added it in version 105 (August 2022). Firefox held out until version 121 (December 2023), making it the last major browser to adopt the feature. As of mid-2026, global browser support exceeds 93%.

Basic Syntax and Specificity

Quick Answer

Write parent:has(child-selector). The :has() pseudo-class itself has zero specificity, but the selectors inside it contribute to the overall rule’s specificity. .card:has(.featured) has specificity (0,2,0) — two class selectors counted.

/* Syntax: element:has(selector-to-check-inside) */ /* Style .card only when it contains an img */.card:has(img) {  display: grid;  grid-template-columns: 160px 1fr;} /* Style a section only when it has an h2 */section:has(h2) {  padding-top: 48px;} /* Style a form group when the input inside is focused */.form-group:has(input:focus) {  box-shadow: 0 0 0 3px rgba(29,158,117,0.25);  border-color: #1D9E75;}

The selector inside :has() is evaluated relative to the element being tested, not the document root. When you write .card:has(img), the browser checks whether any img element exists within each .card element.

/* :has() itself has 0 specificity   but arguments inside it ARE counted */ /* Specificity: (0,1,0) — .card only */.card:has(p) { }/* Specificity: (0,1,1) — .card + p *//* (p is a type selector = 0,0,1) */ /* Specificity: (0,2,0) — .card + .featured */.card:has(.featured) { } /* :has() is like :is() — takes the specificity   of its most specific argument */ /* Specificity: (0,1,1) — .card + img */.card:has(> img) { }/* The > combinator doesn't add specificity */

Understanding specificity is important when combining :has() with other pseudo-classes. Use :where() inside :has() when you want to check for a child condition without adding specificity from the inner selector. If you are ever unsure how a :has() rule stacks up against other selectors, paste it into the CSS Specificity Calculator to get an immediate side-by-side comparison.

Note: :has() does not accept pseudo-elements (::before, ::after) as its argument. You also cannot nest a second :has() inside the first in the current specification — use combinators and comma-separated arguments instead.

Selecting a Parent Based on Its Child

This is the core capability of :has(). Previously, selecting a parent required JavaScript to read the DOM and toggle a class. Now it is a single CSS rule:

/* ── Before :has() — had to restructure HTML or use JS ── */ /* JavaScript approach (pre-:has()) */document.querySelectorAll('.card').forEach(card => {  if (card.querySelector('img')) {    card.classList.add('has-image');  }});.card.has-image { grid-template-columns: 160px 1fr; } /* ── After :has() — pure CSS, no class toggling ── */.card:has(img) {  grid-template-columns: 160px 1fr;}

The immediate benefits are significant: no JavaScript event listeners, no class toggling on mount/update, no timing issues during hydration in React or other frameworks. The CSS is self-documenting — the rule clearly expresses the intent “style this element when it contains that element”.

Common parent selection patterns

SelectorMeaning
.card:has(img)Cards that contain any image
.card:has(> img)Cards with an image as a direct child
form:has(input:invalid)Forms containing any invalid input
section:has(h2)Sections with an h2 heading
p:has(+ ul)Paragraphs immediately followed by a list
.nav-item:has(a[href="/"])Nav items containing a link to the homepage

Styling Form Labels When Input Is Checked or Invalid

Quick Answer

Use label:has(input:checked) to style a label when its checkbox or radio is checked — applying colour, background, and border to the entire label group. Use label:has(input:invalid) for validation styling. Both are zero-JavaScript, declarative, and work with any HTML structure.

Form UX is where :has() has the most immediate practical value. Custom checkbox and radio styling has always required JavaScript or the “label after input” trick using the + adjacent sibling selector. With :has(), the label can precede the input, follow it, or wrap it — and still be styled based on the input’s state.

/* ── Style labels based on their input state ── */ /* Label highlights when checkbox is checked */label:has(input[type="checkbox"]:checked) {  background: rgba(29,158,117,0.08);  border-color: #1D9E75;  color: #1D9E75;} /* Label turns red when input is invalid */label:has(input:invalid:not(:placeholder-shown)) {  color: #dc2626;} label:has(input:invalid:not(:placeholder-shown)) input {  border-color: #dc2626;  outline-color: #dc2626;} /* Label glows when its input is focused */.form-group:has(input:focus-visible) {  box-shadow: 0 0 0 3px rgba(29,158,117,0.3);  border-color: #1D9E75;  border-radius: 4px;}

The input:invalid:not(:placeholder-shown) compound selector is a useful pattern: it matches inputs that are invalid but only after the user has typed something (the placeholder is no longer showing). This prevents the red error state from appearing on a fresh, empty form before the user has interacted.

Entire form sections that react to state

Beyond individual labels, you can use :has() to style an entire fieldset or form section based on the validation state of the inputs it contains:

/* Style the whole section when any input inside is invalid */.form-section:has(input:invalid:not(:placeholder-shown)) {  background: rgba(220, 38, 38, 0.04);  border-color: #dc2626;} .form-section:has(input:invalid:not(:placeholder-shown)) .section-title {  color: #dc2626;} /* Style the submit button when the form is fully valid */form:not(:has(input:invalid)) .submit-btn {  background: #1D9E75;  cursor: pointer;} form:has(input:invalid) .submit-btn {  background: #9ca3af;  cursor: not-allowed;}

When choosing red for error states or green for valid states, always verify the colour meets WCAG contrast requirements against the background — the Color Contrast Checker gives you an instant pass/fail against AA and AAA thresholds, which matters especially since error-state colours often pair light text on coloured backgrounds.

Card Layouts That Adapt When Images Are Present

Content management systems often produce card grids where some items have featured images and others don’t. Designing cards that look good in both states required either two separate card templates or JavaScript class injection. :has() makes this a purely CSS concern:

/* ── Card layout adapts based on whether image exists ── */ /* Base card — text-only layout */.card {  display: flex;  flex-direction: column;  padding: 1.5rem;  border: 1px solid var(--color-border);  border-radius: 8px;} /* When the card has an image — switch to side-by-side */.card:has(img) {  flex-direction: row;  align-items: flex-start;  padding: 0;  gap: 0;} .card:has(img) img {  width: 200px;  flex-shrink: 0;  object-fit: cover;  border-radius: 8px 0 0 8px;} .card:has(img) .card__body {  padding: 1.5rem;} /* Cards WITHOUT an image get a coloured top border */.card:not(:has(img)) {  border-top: 3px solid var(--color-accent);}

The key pattern here is using :has(img) to apply the side-by-side layout and :not(:has(img)) as the base / text-only layout. This is genuinely declarative responsive design at the component level — the card adapts to its own content without any orchestration from JavaScript or a parent component. To prototype the card grid layout itself, the Flexbox Playground is useful for quickly testing alignment and gap combinations before writing the final CSS.

:has() With Other Combinators (>, ~, +)

Quick Answer

:has() accepts full CSS selector syntax inside it, including all combinators. :has(> .direct-child) checks only immediate children. h2:has(+ p)selects headings immediately followed by a paragraph — making sibling selection work “backwards” for the first time in CSS.

/* :has() works with all CSS combinators */ /* Direct child only (>) */.parent:has(> .direct-child) { } /* Adjacent sibling (+) *//* Selects h2 when immediately followed by a p */h2:has(+ p) {  margin-bottom: 8px; /* tighten spacing before intro paragraph */} /* General sibling (~) *//* Selects a section when any following sibling is a .footnote */section:has(~ .footnote) {  padding-bottom: 48px;} /* Descendant (space — default) */.article:has(.code-block) {  max-width: 900px; /* wider layout for code-heavy posts */} /* Multiple selectors inside :has() (comma-separated OR) */.card:has(img, video, iframe) {  /* Matches if card has img OR video OR iframe */  border: none;  padding: 0;}

The h2:has(+ p) pattern is particularly useful for editorial typography: when a heading is immediately followed by an introductory paragraph (a lede), you can tighten the space between them to create a visual grouping — without affecting headings followed by other elements like code blocks or lists.

The sibling combinator variant (:has(~ .sibling)) effectively creates a “preceding sibling” selector — something CSS never had before. This is genuinely new expressive power: you can now style an earlier element based on a later sibling, reversing the usual order of CSS selection.

:has() With :not(), :is(), :where()

/* :has() combined with :not() */ /* Style cards WITHOUT an image (inverse) */.card:not(:has(img)) {  background: var(--color-surface);  padding: 2rem;} /* Select fieldsets that have NO invalid inputs */fieldset:not(:has(input:invalid)) {  border-color: #1D9E75;} /* Dim sections that don't contain the target heading */.section:not(:has(#current-section)) {  opacity: 0.4;}
/* :has() with :is() — match multiple parents */ /* Match any heading level that directly precedes a paragraph */:is(h1, h2, h3, h4):has(+ p) {  margin-bottom: 8px;} /* :has() inside :is() — match either condition */:is(.card:has(img), .card:has(video)) {  /* Cards with image OR video */  padding: 0;} /* :has() with :where() — zero-specificity parent check */:where(.card):has(.featured-badge) {  order: -1; /* move featured cards first in grid */  border: 2px solid gold;}/* :where() contributes 0 specificity,   making this easy to override */

The combination of :is(h1, h2, h3, h4):has(+ p) is a good example of concise, maintainable CSS that replaces four separate rules. The :is() pseudo-class takes the specificity of its most specific argument, so this rule has the same specificity as h1:has(+ p) — something to be aware of when specificity conflicts arise.

:where() is useful when you want the has-check without adding specificity. A rule like :where(.card):has(.featured) has a specificity of just (0,1,0) — the .featured check adds zero specificity from :where() wrapping, making it easy for more specific rules to override the styles.

Quantity Queries — Counting Children With :has()

Quick Answer

Use :has(:nth-child(N)) to check if a container has at least N children. .grid:has(:nth-child(4)) matches grids with 4 or more items. Combine with :not(:has(:nth-child(N+1))) to check for exactly N items. This is the CSS-only equivalent of JavaScript’s children.length check.

/* ── Quantity queries with :has() ── */ /* Grid with at least 4 items → 4 columns */.grid:has(:nth-child(4)) {  grid-template-columns: repeat(4, 1fr);} /* Grid with at least 3 but fewer than 4 items → 3 columns */.grid:has(:nth-child(3)):not(:has(:nth-child(4))) {  grid-template-columns: repeat(3, 1fr);} /* Grid with exactly 1 item → full width */.grid:has(:nth-child(1)):not(:has(:nth-child(2))) {  grid-template-columns: 1fr;} /* Alert banner when list has more than 5 items */.todo-list:has(:nth-child(6))::before {  content: "You have more than 5 items";  display: block;  color: #dc2626;  font-size: 0.875rem;  margin-bottom: 8px;}

Quantity queries unlock truly adaptive grid layouts: a grid that automatically switches between 1, 2, 3, or 4 columns based on how many items are in it — without JavaScript counting children or passing props. This is especially valuable in content-managed systems where the number of items in a grid is unpredictable. To build the underlying grid-template-columns rules for each count, use the CSS Grid Generator — then drop the output into each :has(:nth-child(N)) rule.

Page-Level Patterns: body:has()

When scoped to body or a high-level container, :has() enables page-level state management that previously required JavaScript to update CSS classes on <body>:

/* Powerful: style the body based on what it contains */ /* Dark mode for the entire page when a dark component is open */body:has(.modal[open]) {  overflow: hidden; /* prevent background scroll */} body:has(.sidebar--open) .main-content {  margin-left: 280px; /* push content when sidebar is open */  transition: margin-left 0.3s;} /* Different page layout when a hero section is present */body:has(.hero-section) .site-header {  position: absolute;  top: 0;  left: 0;  right: 0;  background: transparent;  color: white;}

The body:has(.modal[open]) pattern is particularly clean: it prevents background scrolling without any JavaScript adding or removing overflow: hidden on the body. As long as the modal element has the open attribute, the scroll lock applies. When the attribute is removed, scroll is restored automatically.

Performance note: Selectors at the body or html level are evaluated globally on every DOM change. If the selector inside :has() is broad (e.g., body:has(*)), this can cause significant recalculation overhead. Keep body-level :has() selectors specific: body:has(.modal[open]) is fine; body:has(input) is not.

JavaScript Equivalent Comparison

Here is a side-by-side look at common patterns that :has() replaces in JavaScript:

/* ── What :has() replaces ── */ /* BEFORE: JavaScript class-toggle */const cards = document.querySelectorAll('.card');cards.forEach(card => {  if (card.querySelector(':invalid')) {    card.classList.add('has-error');  }}); /* AFTER: CSS :has() — no JavaScript */.card:has(:invalid) {  border-color: #dc2626;  box-shadow: 0 0 0 3px rgba(220,38,38,0.15);} /* Practical replacements */ /* JS: checked state management */document.querySelector('input').addEventListener('change', e => {  e.target.closest('label').classList.toggle('checked', e.target.checked);});/* CSS :has() replacement */label:has(input:checked) { background: rgba(29,158,117,0.1); } /* JS: active link parent highlighting */navLinks.forEach(link => {  if (link.href === window.location.href) {    link.closest('li').classList.add('active');  }});/* CSS :has() replacement */nav li:has(a[aria-current="page"]) { background: rgba(29,158,117,0.1); }

The JavaScript equivalents require event listeners, mutation observers, or lifecycle hooks in frameworks — all of which add code complexity, bundle weight, and potential timing bugs. The CSS versions are declarative, synchronous with rendering, and immune to hydration mismatches in server-rendered React or other frameworks.

Performance: When :has() Is Expensive

:has() requires the browser to look ahead into descendants when evaluating a selector — a more complex operation than standard descendant selectors. Modern browsers (Chrome, Firefox, Safari) have highly optimized implementations, but there are cases where performance can degrade:

/* Performance: be careful with root-level :has() */ /* AVOID: this recalculates on every DOM mutation */html:has(input:focus) { }body:has(.any-element) { } /* PREFER: scope to component level */.form:has(input:focus) { }.card:has(img) { }.nav:has([aria-current]) { } /* The narrower the scope, the faster the recalculation */
PatternPerformanceReason
.card:has(img)FastScoped to component, narrow element count
body:has(input:focus)SlowEvery focus event recalculates the entire tree
.form:has(input:focus)FastScoped to form element only
html:has(.active)SlowDOM mutations anywhere trigger recalculation
nav:has(.active)FastOnly recalculates when nav subtree changes

The rule of thumb: scope your :has() selectors as close to the component as possible. Avoid html:has() and body:has() with dynamic arguments — use them only for stable, infrequently changing states like modal open/close or sidebar toggle.

Browser Support & Fallbacks

BrowserMin versionRelease date
Safari15.4+March 2022
Chrome / Edge105+August 2022
Firefox121+December 2023
Global (mid-2026)~93%Production-ready without polyfill
/* Progressive enhancement with @supports */@supports selector(:has(img)) {  .card:has(img) {    grid-template-columns: 160px 1fr;  }} /* Without @supports, .card always uses the base layout *//* — a safe, readable fallback */

At 93% global support, :has() is one of the most widely available modern CSS features. For the 7% of browsers without support, the graceful degradation strategy is to write base styles that work without the :has() enhancement and use @supports selector(:has(img)) to layer on the adaptive styles. In most real-world applications, the 7% gap does not justify a polyfill. :has() also pairs powerfully with CSS container queries — the CSS Container Query Builder lets you prototype @container rules that work alongside :has() checks for fully context-aware, content-aware component styling.

Frequently Asked Questions

What is the CSS :has() selector?

:has() is a relational pseudo-class that matches an element if the selectors inside it match any element reachable from it. It acts as a parent selector: .card:has(img) selects .card elements that contain an img. It has full support in all major browsers since December 2023.

How do I select a parent element in CSS?

Use :has(). Write the parent selector followed by :has(child-selector). Example: .card:has(img) { flex-direction: row; } — this styles any .card that contains an img. No JavaScript class toggling needed.

What browsers support CSS :has()?

Chrome 105+, Edge 105+, Firefox 121+, and Safari 15.4+. Global browser support exceeds 93% as of mid-2026. It is fully production-ready without any polyfill for the vast majority of projects.

What is the specificity of :has() in CSS?

The :has() pseudo-class itself has zero specificity. The selectors inside it are counted in the overall rule’s specificity. .card:has(.featured) has specificity (0,2,0) — both .card and .featured each contribute one class selector.

Can :has() be used with combinators in CSS?

Yes. :has() accepts full selector syntax including > (direct child), + (adjacent sibling), and ~ (general sibling). h2:has(+ p) selects headings immediately followed by a paragraph — effectively a preceding-sibling selector, which was previously impossible in pure CSS.

How do I style a form label when its input is invalid?

label:has(input:invalid:not(:placeholder-shown)) { color: red; } — This selects labels containing an invalid input that the user has already interacted with. The :not(:placeholder-shown) prevents the error state from appearing on untouched empty fields.

How do I use :has() for quantity queries?

:has(:nth-child(N)) matches if the element has at least N children. .grid:has(:nth-child(4)) fires when there are 4+ items. Combine with :not(:has(:nth-child(5))) to target exactly 4 children. This replaces JavaScript’s children.length checks for adaptive grid layouts.

Is CSS :has() a performance concern?

Keep :has() scoped to components, not root elements. .card:has(img) is fine. body:has(input:focus) can be expensive because any focus event triggers a full-document recalculation. Use body:has() only for infrequent state changes like modal open/sidebar toggle.