Skip to main content
Ganesh Joshi
Back to Blogs

CSS :has() selector: practical uses for styling

February 15, 20265 min read
Tutorials
CSS code on screen

The :has() selector is often called a "parent selector," but it is more general: it matches an element that contains (or otherwise matches) something inside it. That lets you style a container when it has a certain child, or style based on sibling relationships, without JavaScript.

Basic syntax

An element matches :has() if it has a descendant matching the inner selector:

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

/* Form group with an error message */
.field:has(.error) {
  border-color: red;
}

/* Section with a heading */
section:has(h2) {
  border-top: 1px solid #ccc;
}

The element you style is the one that "has" the thing; the inner selector is what must exist inside it.

Direct child selector

Use > to match only direct children:

/* Card with a direct child image (not nested) */
.card:has(> img) {
  padding: 0;
}

/* List item with a direct child link */
li:has(> a) {
  list-style: none;
}

Styling based on form state

Combine :has() with :checked, :focus, :invalid, or :disabled:

Checked state

/* Style label when checkbox is checked */
label:has(input:checked) {
  font-weight: bold;
  color: var(--primary);
}

/* Highlight selected option */
.option:has(input:checked) {
  background: var(--selected-bg);
  border-color: var(--primary);
}

Focus state

/* Highlight form row when input is focused */
.form-row:has(input:focus) {
  background: var(--focus-bg);
}

/* Show label differently when input has focus */
.field:has(input:focus) .label {
  color: var(--primary);
  transform: translateY(-4px);
}

Validation state

/* Show error styling when input is invalid */
.field:has(input:invalid) {
  border-color: var(--error);
}

/* Show success styling when valid */
.field:has(input:valid:not(:placeholder-shown)) {
  border-color: var(--success);
}

Card patterns

Card with image

.card {
  padding: 1.5rem;
}

/* No padding when card has image */
.card:has(> .card-image) {
  padding: 0;
}

.card:has(> .card-image) .card-content {
  padding: 1.5rem;
}

Card with badge

/* Add visual indicator for featured cards */
.card:has(.featured-badge) {
  border: 2px solid var(--primary);
  box-shadow: 0 4px 12px rgb(var(--primary-rgb) / 0.2);
}

Empty state

/* Style container when it has no items */
.list:not(:has(.item)) {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 200px;
}

.list:not(:has(.item))::after {
  content: 'No items found';
  color: var(--muted);
}

Adjacent sibling patterns

:has() can look at following siblings with + and ~:

Heading followed by paragraph

/* Tighten spacing when heading is followed by paragraph */
h2:has(+ p) {
  margin-bottom: 0.5rem;
}

/* Normal spacing when no paragraph follows */
h2:not(:has(+ p)) {
  margin-bottom: 1.5rem;
}

Input with adjacent error

/* Style input when it has an error message following */
input:has(+ .error-message) {
  border-color: var(--error);
  background: var(--error-bg);
}

List item with sublist

/* Style list item that contains a nested list */
li:has(> ul),
li:has(> ol) {
  padding-bottom: 0;
}

Navigation patterns

Active nav section

/* Style nav section containing active link */
.nav-section:has(.active) {
  background: var(--active-section-bg);
}

/* Expand nav section with active item */
.nav-section:has(.active) .nav-items {
  display: block;
}

Dropdown with content

/* Style dropdown trigger when dropdown has items */
.dropdown:has(.dropdown-item) .dropdown-trigger {
  cursor: pointer;
}

/* Disable when empty */
.dropdown:not(:has(.dropdown-item)) .dropdown-trigger {
  cursor: not-allowed;
  opacity: 0.5;
}

Layout patterns

Sidebar detection

/* Adjust main content when sidebar exists */
.page:has(.sidebar) .main {
  margin-left: var(--sidebar-width);
}

/* Full width when no sidebar */
.page:not(:has(.sidebar)) .main {
  max-width: 100%;
}

Grid adjustments

/* Single column when only one item */
.grid:has(> :only-child) {
  grid-template-columns: 1fr;
  max-width: 600px;
  margin: 0 auto;
}

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

Combining :has() with other selectors

:has() with :not()

/* Style paragraphs not inside articles */
p:not(article:has(&) *) {
  /* This is complex; usually easier with direct selectors */
}

/* More practical: items without children */
.container:has(.item):not(:has(.item .item)) {
  /* Has items but no nested items */
}

:has() with :is() and :where()

/* Match multiple conditions */
.card:has(:is(img, video, iframe)) {
  aspect-ratio: 16/9;
}

/* Lower specificity with :where() */
.card:has(:where(img, video)) {
  overflow: hidden;
}

Performance considerations

:has() is well-optimized in modern browsers, but avoid:

/* Too broad - matches entire document */
body:has(*) { }

/* Very complex nested :has() */
.a:has(.b:has(.c:has(.d))) { }

Keep selectors focused:

/* Good - scoped to specific elements */
.card:has(img) { }
.form-field:has(input:focus) { }

Browser support

:has() is supported in:

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

Use progressive enhancement:

/* Default style */
.card {
  padding: 1rem;
}

/* Enhanced with :has() */
.card:has(img) {
  padding: 0;
}

Browsers that do not support :has() ignore the rule and keep the default.

Common patterns summary

Pattern Selector
Parent has child .parent:has(.child)
Parent has direct child .parent:has(> .child)
Element with checked input label:has(input:checked)
Element with focused input .field:has(input:focus)
Element followed by sibling h2:has(+ p)
Element without children .list:not(:has(.item))

Summary

:has() enables parent and sibling selection in CSS:

  1. Match parents containing specific children
  2. Style based on state (checked, focus, invalid)
  3. Adjust layout based on content presence
  4. Combine with :not() for inverse conditions
  5. Use progressive enhancement for older browsers

The MDN :has() reference has the full specification and additional examples.

Frequently Asked Questions

The :has() selector matches an element that contains a descendant (or sibling) matching the inner selector. It is often called a parent selector because you can style a parent based on its children.

Yes, :has() is supported in Chrome, Firefox, Safari, and Edge. It is safe to use in production with progressive enhancement.

No. :has() can match following siblings with :has(+ sibling), but not previous siblings. It looks at descendants and following siblings only.

For typical selectors, :has() is fast. Avoid very broad selectors like body:has(*) that force matching across the entire document.

Use :has() for styling based on DOM structure or element state (checked, focus). Use JavaScript when you need complex logic, async conditions, or to modify the DOM.

Related Posts