Turning Learners Into Developers
Codekilla
CODEKILLA
CSS 8 min

CSS :has() Pseudo-Class Guide

Read on to explore css :has() pseudo-class guide — a beginner-friendly walkthrough by Codekilla.

Rahul Chaudhary Thu Apr 30 2026
What is :has()?

The :has() pseudo-class is CSS's first parent selector — it lets you style an element based on what's inside it or what comes after it. Instead of only selecting children based on their parents (like .parent > .child), you can now flip the logic and select parents based on their children. Think of it as a conditional statement: "If this element has X inside it, apply these styles."

Before :has(), you needed JavaScript to achieve this kind of parent-based styling. Now it's pure CSS, and it's supported in all modern browsers (Safari 15.4+, Chrome 105+, Firefox 121+). This pseudo-class opens up powerful layout and state-driven styling patterns that were previously impossible or required hacky workarounds.

Why It Matters
  • Eliminates JavaScript dependencies — Patterns like "style a card differently if it contains an image" no longer need DOM manipulation or class-toggling scripts.
  • Conditional layouts — Build responsive components that adapt based on content, not just viewport size.
  • Form validation UX — Style form containers based on input states (:invalid, :checked) without touching the input elements themselves.
  • Cleaner component architecture — Parent components can respond to child states, making CSS more powerful and reducing the need for utility classes.
  • Performance boost — Browser-native selection is faster than JavaScript observers watching for DOM changes.
Basic Syntax and Selection

The :has() pseudo-class takes a relative selector as its argument. The element you're selecting is the one that "has" the specified descendant or sibling.

css
/* Select any <article> that contains an <img> */
article:has(img) {
  border: 2px solid blue;
  background: #f0f8ff;
}

/* Select form that has an invalid input */
form:has(input:invalid) {
  border-left: 4px solid red;
}

/* Select card that has both heading and image */
.card:has(h2):has(img) {
  display: grid;
  grid-template-columns: 1fr 2fr;
}

You're not styling the img or input — you're styling their ancestors based on their presence or state. The selector inside :has() can be any valid CSS selector, including pseudo-classes, attribute selectors, and combinators.

Parent Selection Based on Children

This is the most common use case — styling containers differently depending on what content they hold.

css
/* Card with featured image gets special treatment */
.card:has(.featured-image) {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 2rem;
}

/* List items containing links get hover effects */
li:has(a) {
  padding-left: 1rem;
  border-left: 3px solid transparent;
  transition: border-color 0.2s;
}

li:has(a):hover {
  border-left-color: #667eea;
  background: #f7f7f7;
}

/* Section with no content gets hidden */
section:not(:has(*)) {
  display: none;
}

Notice the last example uses :not(:has(*)) to target empty sections. This pattern is perfect for dynamic content where some containers might be empty based on user data or API responses.

Sibling-Based Selection

You can use combinators inside :has() to select elements based on their siblings, not just descendants. This is where it gets really powerful.

css
/* Style a label when its following input is focused */
label:has(+ input:focus) {
  color: #667eea;
  font-weight: 600;
}

/* Heading followed by an image */
h2:has(+ img) {
  margin-bottom: 0.5rem; /* Less space before image */
}

/* Figure that has a figcaption */
figure:has(figcaption) {
  border: 1px solid #ddd;
  padding: 1rem;
}

The + combinator means "immediately following sibling." You can also use ~ for any following sibling. This lets you create contextual styles based on document structure without adding classes.

Form State Management

Forms are where :has() truly shines. You can style form containers, fieldsets, or sections based on input states.

State PatternUse Case
form:has(input:invalid)Show error border on entire form
fieldset:has(input:checked)Highlight selected option groups
div:has(input:focus)Style wrapper when input is active
.form-group:has(input:required:invalid)Show validation feedback on field groups
css
/* Form section with errors */
.form-section:has(input:invalid:not(:placeholder-shown)) {
  background: #fff5f5;
  border-left: 4px solid #e53e3e;
  padding-left: 1rem;
}

/* Checkbox group with checked items */
.checkbox-group:has(input:checked) {
  background: #f0fff4;
  border-color: #38a169;
}

/* File input wrapper when file is selected */
.file-upload:has(input[type="file"]:valid) {
  border-color: #38a169;
}

.file-upload:has(input[type="file"]:valid)::after {
  content: "✓ File selected";
  color: #38a169;
  font-size: 0.875rem;
}

The :not(:placeholder-shown) trick ensures you only show errors after the user has started typing, not on page load.

Complex Conditional Styling

You can chain multiple :has() selectors or combine them with other pseudo-classes for sophisticated conditional logic.

css
/* Article with image AND video */
article:has(img):has(video) {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 2rem;
}

/* Container with more than 5 children (using :nth-child) */
.grid:has(:nth-child(6)) {
  grid-template-columns: repeat(3, 1fr);
}

/* Dark mode toggle affecting entire page */
body:has(#dark-mode:checked) {
  background: #1a202c;
  color: #e2e8f0;
}

body:has(#dark-mode:checked) a {
  color: #63b3ed;
}

The dark mode example shows how a single checkbox can control the entire page's theme without JavaScript — just use :has() on the body or :root element.

Quick Cheat Sheet
NeedReach for
Style parent based on childparent:has(child)
Style based on siblingelement:has(+ sibling)
Form validation stylingform:has(input:invalid)
Conditional layoutscontainer:has(.specific-class)
Empty state handlingelement:not(:has(*))
Multiple conditionsel:has(.a):has(.b)
Checked checkbox parentlabel:has(input:checked)
Common Mistakes
  • Forgetting browser support — While modern browsers support :has(), always check @supports for critical layouts or provide fallbacks for older browsers.

  • Overusing :has() for simple descendant selectors — If you're just styling children, use normal descendant selectors. Don't write .parent:has(.child) .child when .parent .child works fine.

  • Performance concerns with deeply nested :has() — Avoid chaining too many :has() selectors or using them with universal selectors like :has(*) on large DOMs — the browser has to do more work.

  • Not considering empty states — When using :has() for layout changes, always test what happens when elements are empty or missing. Use :not(:has(*)) to handle empty containers.

  • Forgetting specificity rules:has() has the same specificity as a class selector, but the selectors inside it also count. div:has(.foo) has higher specificity than just div.

  • Using :has() with pseudo-elements — You can't select pseudo-elements with :has(). This won't work: div:has(::before). Pseudo-elements aren't in the DOM.

💡 Think Like a Programmer: The :has() pseudo-class is CSS's answer to conditional rendering — instead of writing "if this, then that" in JavaScript, you're declaring "when this exists, style that" directly in your stylesheets. It's declarative logic for visual states.

// was this useful?
Did this article answer your question?
// CSS · published by Codekilla
// related articles

Keep Reading