Topic 11.2
:has() in Practice
In one line
:has() matches an element when something inside it, or after it, matches the selector you give; that lets CSS style a form from the state of its inputs, a parent from its children, and a list from how many items it has, all without JavaScript.
Think of it like this
A bag check at a concert. The guard doesn't style the people; she looks inside each bag and puts a sticker on the bag's owner if the bag has a water bottle. :has() works the same way: it looks inside (or next to) an element and marks the element itself when it finds a match. For years CSS could only look down the tree from a parent; :has() lets a rule depend on what is inside.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- `:has()`
- A pseudo-class that matches an element if the selector inside the brackets finds a match relative to it.
- Relative selector
- A selector read starting from another element, like
> img(a direct child) or+ p(the next sibling). - Subject
- The element a selector actually styles. In
.card:has(img), the subject is the card, not the image. - Sibling
- An element with the same parent.
+means the very next sibling;~means any later sibling. - `:invalid`
- Matches a form field whose value breaks its rules, for example an empty
requiredfield. - `:user-invalid`
- Like
:invalid, but only after the user has changed the field or tried to submit, so errors don't show on a fresh form. - Quantity query
- A selector that styles a list based on how many items it has, using
:nth-child()inside:has(). - Unforgiving list
- A selector list where one invalid entry makes the whole thing invalid.
Step by step
01Read :has() as "that has"
Read .field:has(input:focus) aloud: "a .field that has a focused input inside it". The rule styles the .field. This is how you highlight a whole form row while its input is focused.
.field:has(input:focus-visible) {
background: #eff6ff;
}02React to form states
Mark required fields, show errors only after the user has typed, and dim the submit button while any field is invalid or the terms box is unticked. No JavaScript is needed for any of these looks. (The server must still validate: CSS only changes what people see.)
.field:has(:required) label::after { content: " *"; color: #dc2626; }
.field:has(input:user-invalid) input { border-color: #dc2626; }
form:has(input:invalid, #terms:not(:checked)) button {
opacity: 0.5;
}03Style a parent by its children
A card with an image becomes a two-column grid; a card without one gets a dashed border. The HTML needs no extra classes.
.card:has(> img) {
display: grid;
grid-template-columns: 64px 1fr;
gap: 12px;
}
.card:not(:has(img)) { border-style: dashed; }04Count items with a quantity query
If a 4th item exists, the list has at least 4. If the 3rd item is also the last, the list has exactly 3.
ul:has(> li:nth-child(4)) { columns: 2; } /* 4 or more */
ul:has(> li:nth-child(3):last-child) { color: teal; } /* exactly 3 */05Look sideways at siblings
:has(+ x) checks the next sibling, which gives CSS a "previous sibling" selector at last: h2:has(+ .subtitle) removes the heading's bottom margin only when a subtitle follows it.
h2:has(+ .subtitle) { margin-bottom: 0; }06Plan for support and speed
Every current browser supports :has() (Baseline 2023). For older ones, wrap enhancements in @supports selector(:has(a)) { ... } so the base styles still work. Keep the subject specific (.field:has(...) rather than *:has(...)), so the browser has fewer elements to recheck when something changes.
Try it yourself
- 1
Make the error visible
In the first example, type
asha(no @) into the email box and then click outside it. Predict what changes and why it didn't happen before you typed. (:user-invalidwaits for the user.) - 2
Count differently
In the second example, change the quantity query so two columns start at 6 items. Predict which list loses its two columns, then run it.
- 3
Highlight the chosen option
Add three radio buttons wrapped in labels to the third example and write
label:has(input:checked) { font-weight: bold; }. Predict what happens to the old label when you choose another option.
Code & diagrams
Only the field that has a `required` input gets the star. The button stays dim while the email is empty or the box is unticked. Type a bad email and click elsewhere to see `:user-invalid` turn the border red.
Preview
Console
Nothing logged yet.Expected console output
email label marker: " *"
age label marker: none
button opacity at start: 0.5
button opacity when valid: 1Delete the image from the first card and run again: it stops being a grid and gets the dashed border. Add a 4th item to the last list and it loses teal and gains two columns.
Preview
Console
Nothing logged yet.Expected console output
card 1 display: grid
card 2 border-style: dashed
2 items, columns: auto
5 items, columns: 2
exactly 3, colour: rgb(0, 128, 128)The checkbox is not an ancestor of the items, but `.page:has(#compact:checked)` turns the page into the subject, and the descendant `.item` follows. Click the checkbox to toggle it yourself.
Preview
Console
Nothing logged yet.Expected console output
normal padding-top: 14px
compact padding-top: 4pxBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Put a pseudo-element inside :has()
In the second example, add .card:has(p::before) { outline: 3px solid red; }.
Break #2
Expect :has() to style the inner element
Write .field:has(input:invalid) { border-color: red; } and expect the input's border to turn red.
Myth vs fact
Myth
:has() is only a parent selector.
Fact
It matches based on any relative selector: descendants (:has(img)), direct children (:has(> img)) and later siblings (:has(+ p), :has(~ p)). That makes it a previous-sibling selector too.
Myth
:has() is too slow to use.
Fact
Browsers optimise it heavily and it is fine for normal use. Cost grows when the subject is very broad (*:has(...) or body:has(...) that changes often), so anchor it to specific elements.
Myth
Styling invalid fields with :invalid is good UX.
Fact
:invalid matches from the first paint, so an empty required field looks wrong before the user typed anything. Use :user-invalid for error looks.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
:has()shipped in Safari 15.4 (2022), Chrome 105 (2022) and Firefox 121 (December 2023), making it Baseline 2023. For browsers without it,@supports selector(:has(a))lets you wrap the enhancement. - ▸
Invalidation is the hard part: when a DOM change happens deep inside, the browser must find which ancestors'
:has()results changed. Engines keep extra bookkeeping (Chromium and WebKit track which elements could be affected by a:has()argument). Keeping the argument simple (> img,:checked) and the subject narrow keeps that work small. - ▸
:has()was originally specified with a forgiving list, but in late 2022 it was changed to unforgiving to avoid ambiguity with jQuery's own:has(). Wrap uncertain parts in:is()if you need forgiving behaviour::has(:is(.a, :unknown-thing)). - ▸
:user-invalidreached all major browsers in 2023. Error text tied to it should still be announced to screen readers: link it to the input witharia-describedby, as the HTML course's accessibility phase (/html, Phase 7) explains.
Remember this
- 1
The syntax is
A:has(B): "anAthat has aBinside it".Bis a relative selector: it is read starting fromA..card:has(img)is a card with an image anywhere inside;.card:has(> img)is a card whose direct child is an image;h2:has(+ p)is anh2immediately followed by a paragraph (a sibling, not a descendant). The element that gets styled is alwaysA, called the subject of the selector. (Phase 1 introduced:has()with:is(),:not()and:where(); this topic is about using it well.) - 2
Form states are the most common real use. Inputs already have state pseudo-classes:
:checked,:disabled,:focus-visible,:invalid(fails its rules, like an emptyrequiredfield), and:user-invalid(invalid after the user has interacted with it, so fresh forms don't shout errors). With:has(), the wrapper can react:.field:has(input:user-invalid)gives the whole field a red border and shows its error text,.field:has(:required) label::after { content: " *" }marks required labels, andform:has(:invalid) button[type=submit]dims the submit button until everything is valid. - 3
Parent styling means styling a container based on what it contains.
.card:has(img)switches a card to a two-column layout when there's a picture;.card:not(:has(img))styles cards without one;li:has(> ul)marks menu items that open a sub-menu;label:has(input:checked)highlights the chosen option in a group of radio buttons. You no longer need an extra class likecard--with-imagethat someone must remember to add. - 4
Quantity queries style a list by how many items it has.
li:nth-child(4)exists only when there are at least 4 items, soul:has(> li:nth-child(4))means "a list with 4 or more items". Add:last-childto get an exact count:ul:has(> li:nth-child(3):last-child)is a list with exactly 3.ul:not(:has(> li:nth-child(2)))is a list with at most one item. Use them to switch a long list to two columns or a short one to a single row. - 5
:has()can also act globally:html:has(dialog[open])stops the page scrolling while a dialog is open (overflow: hidden), andbody:has(#compact:checked)makes a checkbox switch the whole page into a compact mode. It's powerful, but it puts page-wide state in CSS, so keep these few and obvious. - 6
Rules to remember. The specificity of
:has()is the specificity of its most specific argument, like:is(). You can't put:has()inside another:has(), and you can't put pseudo-elements like::beforeinside it. Its argument list is unforgiving: if one selector in the list is invalid, the whole:has()is invalid. To test for support, use@supports selector(:has(a)).
Explain it without notes
What does :has() do, and which element does a selector like .card:has(img) style?
Show how :has() can style a form based on its inputs' state, and why :user-invalid is better than :invalid for error styles.
Explain a quantity query that matches lists with at least 4 items and one that matches exactly 3 items.
What are the restrictions and the specificity rules of :has()?
Practice
Write CSS that gives a <figure> a grey background only when it contains a <figcaption>.
Write CSS so a .plan label gets a 2px indigo border when the radio button inside it is checked.
Write a quantity query that makes a .tags list display as a single flex row when it has 3 or fewer items.
Trade-offs
- ↔
:has()removes JavaScript and helper classes for many states, but it moves logic into selectors that are harder to search for and debug than a class name added by a script. - ↔
Global switches like
html:has(dialog[open])are elegant, but every such rule makes the browser recheck a large subject when things change; keep them few.
Done when you can
I can read
A:has(B)and name the subject.I can style a form wrapper from its inputs' states with
:has().I know when to use
:user-invalidinstead of:invalid.I can write quantity queries for "at least n" and "exactly n".
I can use
:has(+ x)as a previous-sibling selector.I know the restrictions: no nested
:has(), no pseudo-elements, unforgiving lists.