Featured image of post Creative CSS Recipes with Parent Selector :has() Featured image of post Creative CSS Recipes with Parent Selector :has()

Creative CSS Recipes with Parent Selector :has()

Avoid wrapping components in dynamic hook flags by applying styles natively using CSS :has().

Creative CSS Recipes with Parent Selector :has()

For years, CSS developers dreamed of a parent selector — a way to style an element based on its children. JavaScript and extra class names were the only workarounds. Then came :has(), the CSS pseudo-class that finally makes “if this element contains X, style it accordingly” a native reality.

As of 2026, :has() is supported in all major browsers, opening a world of markup simplification and creative styling without touching JavaScript.


How :has() Works

The :has() pseudo-class takes a relative selector list as its argument and matches an element if any of the given selectors match at least one descendant or sibling.

/* Style a card differently if it contains an image */
.card:has(img) {
  padding: 0;
}

/* Style a label when its input is focused */
label:has(input:focus) {
  color: blue;
}

/* Style a form group when any field is invalid */
.field-group:has(:invalid) {
  border-color: red;
}

The selector is evaluated in document order, not in a reverse-traversal sense — but it effectively lets you look at children, descendants, and even subsequent siblings from the current element.


Recipe 1: Form Validation Styling

One of the most practical uses is form state styling. Instead of tracking :valid / :invalid on each input and using JavaScript to toggle parent classes, :has() handles it natively.

.form-group:has(input:invalid) .error-message {
  display: block;
}

.form-group:has(input:invalid) {
  border-left: 3px solid #e74c3c;
}

.form-group:has(input:valid) {
  border-left: 3px solid #2ecc71;
}

.form-group:has(input:placeholder-shown) {
  border-left: 3px solid #ccc;
}

This snippet shows/hides error messages and colors borders based purely on input state, with zero JavaScript.


Recipe 2: Dynamic Card Layouts

Cards with images often need different padding than text-only cards. :has() makes this trivial.

.card {
  padding: 24px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.card:has(img) {
  padding: 0;
}

.card:has(img) .card-content {
  padding: 16px 24px 24px;
}

.card:has(.badge) {
  position: relative;
  overflow: visible;
}

Now a card automatically adjusts its layout when it contains an image or badge, without template-level conditionals.


Recipe 3: Star Rating with Sibling Awareness

The :has() selector works with both child and sibling combinators, enabling patterns like highlighting preceding stars in a rating widget.

.star-rating {
  display: flex;
  flex-direction: row-reverse;
}

.star-rating label {
  cursor: pointer;
  color: #ccc;
}

.star-rating label:has(~ label:checked),
.star-rating label:checked {
  color: gold;
}

Using the general sibling combinator ~ inside :has(), you can style elements based on siblings that appear later in the DOM — a pattern previously impossible without JavaScript.


Create a filterable gallery where items are hidden or shown based on data attributes, without JavaScript event handlers.

.gallery-item {
  display: block;
}

/* Hide items that do NOT have the 'active' class when filter is on */
.gallery:has(.filter-active) .gallery-item:not(.active) {
  display: none;
}

.gallery:has(.filter-active) {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

By applying a .filter-active class to the filter button container, you toggle the entire gallery layout.


Recipe 5: Navigation and Dropdown Menus

Traditional CSS-only dropdowns required hover-based hacks. With :has(), you can build accessible, state-aware navigation.

.nav-item:has(.dropdown-menu) {
  position: relative;
}

.nav-item:has(.dropdown-menu:hover),
.nav-item:has(.dropdown-trigger:focus-within) {
  background: #f0f0f0;
}

.nav-item:has(.dropdown-menu) .dropdown-menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
}

.nav-item:has(.dropdown-trigger:hover) .dropdown-menu,
.nav-item:has(.dropdown-trigger:focus-within) .dropdown-menu {
  display: block;
}

This keeps the menu open when hovering either the trigger or the menu itself, using only CSS.


Recipe 6: Table Row Highlighting

Highlight an entire table row when a specific cell contains a value.

tr:has(td.status-error) {
  background: #fde8e8;
}

tr:has(td.status-success) {
  background: #e8fde8;
}

tr:has(td.amount-high) td.amount {
  font-weight: bold;
  color: #e67e22;
}

This is especially useful for dashboards and admin panels where row-level visual cues improve scanability.


Performance Considerations

While :has() is powerful, it can be expensive if overused or applied to large DOM trees.

  • Avoid deep nesting:has(div span p) triggers complex subtree checks
  • Use simple selectors — prefer :has(img) over :has(img[src^="https"].lazy)
  • Limit scope — apply :has() to a container, not the document root
  • Browser optimizations — modern engines are fast, but thousands of :has() selectors on large pages still cost
/* Prefer this */
.list-item:has(.active) { background: blue; }

/* Over this */
body :has(.active) { background: blue; }

As a rule of thumb, use :has() where it reduces JavaScript complexity, but don’t replace every JS pattern with it.


Conclusion

The :has() pseudo-class is one of the most significant CSS additions in years. It eliminates countless JavaScript workarounds, simplifies templates, and enables styling patterns that were previously impossible in pure CSS.

By adopting :has() in your forms, cards, navigation, tables, and galleries, you write less code, ship less JavaScript, and build more maintainable interfaces. As browser support reaches 100%, there is no reason not to start using :has() today.