By Tim King · Updated
CSS Has Selector Builder
:has() Parent Selector Patterns with Demos
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
- Choose a pattern from the list. Each one is a common real-world use.
- Pick an accent colour to see the highlight change.
- Interact with the demo where it invites it: tick the boxes or hover the row.
- 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
CSS Nesting Converter
Nest flat CSS with & or flatten nested CSS back into plain rules.
Cascade Layers Generator
Set your @layer order and get the blocks or imports to paste.
@supports Rule Generator
Build @supports feature queries with fallbacks and see live support.
Container Query Generator
Build @container queries and cqi units with a resizable live demo.
Checkbox Styler
Custom tick boxes and toggle switches from a real checkbox.