Skip to main content

CSS :has() Selector Guide

The revolutionary "parent selector" that CSS developers waited 20 years for.
Click any code block to copy to clipboard.

What is :has()?

The :has() selector is a CSS pseudo-class that selects an element if any of the selectors passed as parameters match at least one element. It is often called the "parent selector" because it allows you to style a parent based on its children.

Before :has(), there was no way to select a parent element based on what children it contained. You had to use JavaScript. Now, with :has(), you can write pure CSS that responds to DOM structure.

Browser Support (2024+)

Chrome
105+
Safari
15.4+
Firefox
121+
Edge
105+

Supported in all major browsers since late 2023. Safe to use in production.

Before :has()

Required JavaScript to style parents:

// Check if parent has image
if (card.querySelector('img')) {
  card.classList.add('has-image');
}

With :has()

Pure CSS, no JavaScript needed:

/* Style card that has an image */
.card:has(img) {
  padding: 0;
}

Basic Syntax

element:has(selector) { styles }
element

The element to select (parent)

:has()

The functional pseudo-class

selector

What to look for (child/descendant)

Select parent with specific child

Style a parent element based on what children it contains

/* Style figure that contains a figcaption */
figure:has(figcaption) {
  border: 2px solid #e5e7eb;
  padding: 1rem;
  border-radius: 8px;
}

/* Style article that contains an image */
article:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 1rem;
}

Direct child selector

Use > to select only direct children, not all descendants

/* Link that directly contains an image */
a:has(> img) {
  display: block;
  border-radius: 8px;
  overflow: hidden;
}

/* List item with direct button child */
li:has(> button) {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

Multiple conditions

Combine multiple :has() selectors for complex conditions

/* Card with both image AND badge */
.card:has(img):has(.badge) {
  position: relative;
}

/* Form with both email and password fields */
form:has(input[type="email"]):has(input[type="password"]) {
  border: 2px solid #6366f1;
}

Form Validation Patterns

One of the most powerful use cases for :has() is form validation styling without any JavaScript.

Form validation styling

Change form appearance based on input validity

/* Form with any invalid input */
form:has(input:invalid) {
  border-color: #ef4444;
  background: #fef2f2;
}

/* Form with all valid inputs */
form:has(input:valid):not(:has(input:invalid)) {
  border-color: #22c55e;
  background: #f0fdf4;
}

Required field label styling

Style labels for required inputs without JavaScript

/* Label followed by required input */
label:has(+ input:required)::after {
  content: " *";
  color: #ef4444;
}

/* Fieldset containing required fields */
fieldset:has(input:required) {
  border-left: 3px solid #ef4444;
}

Focus-within alternative

Style parent when any child has focus

/* Input wrapper with focused input */
.input-wrapper:has(input:focus) {
  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3);
  border-color: #6366f1;
}

/* Show helper text when input is focused */
.field:has(input:focus) .helper-text {
  display: block;
  opacity: 1;
}

Advanced Patterns

Combining with :not()

Select elements that do NOT contain certain children

/* Card without an image - add padding */
.card:not(:has(img)) {
  padding: 2rem;
}

/* Empty state for lists without items */
ul:not(:has(li)) {
  display: grid;
  place-items: center;
  min-height: 200px;
}
ul:not(:has(li))::before {
  content: "No items found";
  color: #9ca3af;
}

Quantity queries

Change layout based on number of children

/* 2 columns when 4+ items */
.grid:has(> :nth-child(4)) {
  grid-template-columns: repeat(2, 1fr);
}

/* 3 columns when 7+ items */
.grid:has(> :nth-child(7)) {
  grid-template-columns: repeat(3, 1fr);
}

/* Stack first item when 6+ items */
.grid:has(> :nth-child(6)) > :first-child {
  grid-column: span 2;
}

Sibling-based styling

Style siblings based on state of other siblings

/* Dim other items when one is hovered */
.list:has(.item:hover) .item:not(:hover) {
  opacity: 0.5;
  transform: scale(0.98);
}

/* Highlight related items */
.container:has(.primary:hover) .secondary {
  border-color: #6366f1;
  background: #eef2ff;
}

Previous sibling selector

Style elements before another element (impossible before :has())

/* Style all items before the active one */
/* This was impossible in CSS before :has()! */
li:has(~ li.active) {
  opacity: 0.5;
}

/* Style label when its input has error */
label:has(+ input.error) {
  color: #ef4444;
}

Live Interactive Demos

Interact with these demos to see :has() in action:

Form Validation

Form border changes based on input validity

Checkbox Label Styling

Label styling changes when checkbox is checked

Card with/without Image

Code on screen

Card with Image

No padding on container

Card without Image

Has left border accent

Navigation Active State

Nav background changes when an item is active

Quantity-based Layout

Item 1
Item 2
Item 3

Grid changes columns based on item count (1-3: 1col, 4-6: 2col, 7+: 3col)

Sibling Highlight Effect

  • First item
  • Second item
  • Third item
  • Fourth item

Hover any item to dim the others

Real-World Practical Examples

Responsive navigation

Change nav styling based on active state

/* Gradient background when item is active */
nav:has(.nav-item.active) {
  background: linear-gradient(to right, #6366f1, #8b5cf6);
}

/* White text for non-active items when one is active */
nav:has(.nav-item.active) .nav-item:not(.active) {
  color: white;
}

Interactive checkbox labels

Style entire label area based on checkbox state

/* Selected option styling */
.option:has(input:checked) {
  background: #dbeafe;
  border-color: #3b82f6;
}

.option:has(input:checked) .option-title {
  color: #1d4ed8;
  font-weight: 600;
}

/* Disabled option styling */
.option:has(input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}

Modal backdrop control

Control backdrop when modal is open

/* Show backdrop when modal is present */
body:has(.modal.open) {
  overflow: hidden;
}

body:has(.modal.open)::before {
  content: "";
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 40;
}

Dark mode toggle

Apply dark mode based on toggle state

/* When dark mode toggle is checked */
html:has(#dark-mode:checked) {
  color-scheme: dark;
}

html:has(#dark-mode:checked) body {
  background: #1f2937;
  color: #f9fafb;
}

Performance Considerations

When :has() Can Be Slow

The :has() selector requires the browser to check descendants, which can be expensive in certain scenarios:

  • !Broad selectors: *:has(img) checks every element
  • !Deep nesting: div:has(div div div img) searches deeply
  • !Frequent DOM changes: Adding/removing elements triggers re-evaluation

Best Practices

Do

  • Use specific selectors: .card:has(img)
  • Prefer direct child: :has(> img)
  • Combine with classes for clarity
  • Test on low-end devices

Avoid

  • Universal selector: *:has(...)
  • Complex nested selectors
  • Overusing on frequently updated DOM
  • Relying on it for critical layout

Performance Comparison

/* Slower - checks all elements */
*:has(> .active) { ... }

/* Faster - scoped to .nav elements */
.nav:has(> .active) { ... }

/* Fastest - direct child only */
.nav:has(> .nav-item.active) { ... }

Quick Reference

PatternDescriptionExample
A:has(B)A that contains B anywhere.card:has(img)
A:has(> B)A with direct child Bul:has(> li.active)
A:has(+ B)A immediately followed by Blabel:has(+ input:required)
A:has(~ B)A followed by B (any sibling)li:has(~ li.active)
A:not(:has(B))A that does NOT contain B.card:not(:has(img))
A:has(B):has(C)A that contains both B and Cform:has(input):has(button)
A:has(B, C)A that contains B or C.container:has(img, video)