By · Updated

CSS Has Selector Builder

:has() Parent Selector Patterns with Demos

No ratings yet

OneTask is free, with no sign-up, and built by one person.
If this tool saved you time, a coffee helps keep new ones coming.

How the :has() Selector Builder works

The CSS has selector lets a rule match an element by what it contains, which makes it the parent selector CSS lacked for twenty years. Pick a pattern, set an accent colour and the demo shows the rule working, with the HTML and CSS ready to copy.

Before :has(), CSS selectors could only look down and forward. You could style a child based on its parent, or a later sibling based on an earlier one, but never the reverse. A card that should change layout when it holds an image needed an extra class added by hand or by JavaScript. With :has() the card can ask the question itself: .card:has(img) matches every card that contains an image anywhere inside it.

The pseudo-class takes a relative selector list. Whatever you put in the brackets is matched against the element's descendants by default, and a leading combinator changes the direction. :has(> img) checks direct children only, and :has(+ .item) checks the next sibling. That last form is what makes previous-sibling selectors possible.

How to use the CSS has selector builder

  1. Choose a pattern from the list. Each one is a common real-world use.
  2. Pick an accent colour to see the highlight change.
  3. Interact with the demo where it invites it: tick the boxes or hover the row.
  4. Copy the HTML and CSS from the code panel.

The six patterns

Pattern Selector Typical use
Card that contains an image .card:has(img) Two columns only when there is media
Card without an image .card:not(:has(img)) Different accent for text-only cards
Ticked checkbox option .option:has(input:checked) Highlight the whole selected tile
Invalid form field form:has(input:user-invalid) button Dim submit while a field is wrong
Item before the hovered one li:has(+ li:hover) Dock hover that grows the neighbour
List with four or more items .tags:has(> li:nth-child(4)) Change layout past a list size

The quantity query in the last row works because li:nth-child(4) only exists when there are at least four items. Swap in :nth-child(6) for six or more, or combine it with :not() for ranges.

Browser support

:has() is supported in Chrome and Edge 105, Safari 15.4 and Firefox 121, which covers well over 90 percent of browsers in use. For anything older, treat it as progressive enhancement. The page should still work without the rule, and you can wrap it in @supports selector(:has(a)) to keep a fallback separate.

Tips and pitfalls

Keep the argument narrow. The browser has to re-check :has() whenever the DOM below the element changes. body:has(.modal-open) is fine on most pages, but a broad selector such as *:has(*) on a large document is slow. Anchor the subject to a class and keep the inner selector specific.

Specificity comes from the argument. .card:has(#hero) has the specificity of a class plus an ID, just like :is(). A heavy selector inside the brackets can quietly win over rules you expected to override it.

It cannot be nested inside itself. :has(:has(img)) is invalid, and so are pseudo-elements in the argument, such as :has(::before). One level is enough for almost every real layout.

Use :user-invalid for forms. Plain :invalid matches a required field the moment the page loads, before anyone has typed. :user-invalid waits until the user has interacted, which is what people expect from validation styles.

Hover patterns need a fallback on touch. The previous-sibling demo relies on hover, which does not exist on most phones. Make sure the layout still makes sense without the effect.

Whole-page state is possible. html:has(dialog[open]) can lock scrolling while a dialog is open, and body:has(#dark:checked) can switch a theme from a checkbox with no script at all. These are handy, but keep the logic readable for whoever maintains the stylesheet next.

Reading the result

The code panel holds two parts. The HTML block is the demo markup, which you can replace with your own as long as the class names match. The CSS block lists the base component styles first and the :has() rule last, so the rule you came for is always at the bottom and easy to lift on its own.

FAQ

Is :has() really a parent selector?

Yes, in practice. It matches an element based on what it contains, so .card:has(img) styles the card rather than the image. It goes further than a parent selector because it can also look at siblings with + and ~.

Can I select a previous sibling in CSS?

Yes, with :has() and a sibling combinator. li:has(+ li:hover) matches the item directly before the hovered one, and li:has(~ li:hover) matches every earlier item in the same list.

Does :has() slow down my page?

Not noticeably when the selector is specific. Browsers optimise common patterns well. Trouble only starts with very broad rules on large, frequently changing pages, such as *:has(*), so anchor the subject to a class.

What happens in browsers without :has() support?

The whole rule is ignored, as with any unknown selector. Write the base styles so the component works without it, or put alternative rules inside @supports not selector(:has(a)) for older browsers.

See also: all Layout & Spacing tools, Shadows & Motion, Shapes & SVG, UI Components, Forms & Inputs, Colour.

Related tools