:has()
Selects an element based on what it contains or what follows it. Lets you style a parent according to its children.
Example
#.card {
display: grid;
gap: 10px;
}
.card:has(img) {
grid-template-columns: 56px 1fr;
align-items: center;
}<style>
.card { padding: 12px; border-radius: 12px; background: #18181b; }
.card img { width: 56px; height: 56px; border-radius: 12px; }
.card p { margin: 0; color: #a1a1aa; font-size: 14px; }
</style>
<div style="display: grid; gap: 10px;">
<div class="card">
<img src="/collections/astro.png" alt="Astro logo">
<div><strong>Astro</strong><p>A card with an image gets two columns.</p></div>
</div>
<div class="card">
<div><strong>No image</strong><p>This card stays in a single column.</p></div>
</div>
</div>Definition and usage
#The :has() pseudo-class checks whether something matching the given selector exists inside an element or next to it. .card:has(img) selects cards that contain an image, and form:has(:invalid) forms with at least one invalid field. For years, this kind of "parent selector" was considered impossible in CSS.
The selector inside the parentheses is relative to the element. With no combinator it means a descendant at any depth, with > only a direct child (ul:has(> li.active)), and with + or ~ a sibling further along: h2:has(+ p) is a heading followed by a paragraph. This way, :has() also gives you a previous-sibling selector.
The condition is live. label:has(input:checked) changes the label’s look the moment the field gets checked, and body:has(dialog[open]) can lock page scrolling while a dialog is open. Many tasks that used to require JavaScript are handled with a single rule.
Specificity is calculated the same way as in :is(): the weight of the strongest selector in the list. :has() can’t contain another :has() or pseudo-elements. Narrow it down to specific components, since the browser has to re-evaluate very broad rules often.
Syntax
#:has( <relative selector list> )Example selectors
#- Cards that contain an image. The text-only card stays gray.
- Check the box in the third card: the whole card lights up, not just the field.
- The reverse condition: cards without an image.
- The container reacts to the state of a field lying inside it. Check the box and watch the border.
Example selectors in practice
#.card:has(img)
.card:has(img) { background: #7c3aed; color: #fff; }.card:has(input:checked)
.card:has(input:checked) { background: #7c3aed; color: #fff; }.card:not(:has(img))
.card:not(:has(img)) { background: #7c3aed; color: #fff; }.demo:has(input:checked)
.demo:has(input:checked) {
border-color: #a78bfa;
background: rgb(124 58 237 / 0.15);
}More examples
#.field:has(input:invalid) {
border-color: #f43f5e;
}
.field:has(input:invalid) .error {
display: block;
}<style>
.field { display: grid; gap: 6px; padding: 12px; border: 1px solid #3f3f46; border-radius: 12px; }
.field input { padding: 8px 10px; border: 1px solid #52525b; border-radius: 8px; background: #18181b; color: inherit; font: inherit; }
.error { display: none; color: #fda4af; font-size: 13px; }
</style>
<div class="field">
<label for="mail">Email address</label>
<input id="mail" type="email" placeholder="Type something without an @ sign">
<small class="error">That doesn’t look like an email address.</small>
</div>Type text without an @ sign and click outside the field. The border and message change at the level of the whole field, even though the error belongs to the input alone.
Browser support
#Widely available · since 2023
Works in all modern browsers, phones included. You can use it without worry.
- Chrome
- Edge
- Firefox
- Safari
Good practices
#- Narrow
:has()down to a component, e.g..card:has(img). A bare:has(…)with nothing before the colon checks every element on the page. - Older browsers skip a rule with
:has()entirely. Make sure the page still looks reasonable without it, and treat:has()as an enhancement, for example inside@supports selector(:has(a)). - Before adding a class to a parent from JavaScript, check whether
:has()combined with a state pseudo-class, such as:checked,:focus-visible, or:invalid, is enough.
Related entries
#- :is()Matches an element against any selector in a list. Shortens long, repetitive selectors into a single notation.
- :not()Selects elements that match neither the given selector nor any selector in the list.
- :focus-withinMatches an element that either has focus itself or contains an element with focus. Lets you highlight an entire form or card.
- :checkedMatches a checked checkbox, a selected radio button, and a selected option in a select list.
- :invalidMatches form fields whose value breaks an HTML validation rule, such as an empty required field or a malformed email address.
- A + BThe + sign selects element B that sits directly after element A and shares the same parent.
Spotted a mistake or missing an example? Write to us.