CSS :has() Selector: Syntax and Examples
Learn how CSS :has() selects elements through descendant, child, and sibling relationships, with syntax, examples, browser support, and progressive enhancement.
CSS :has() selects an element when at least one relative selector inside its parentheses matches in relation to that element. The element before :has() is the one selected; the contents of the parentheses describe what to look for. Use a bare selector to test descendants, > for direct children, and + or ~ for following siblings.
The precise term is relational pseudo-class. It is often called a parent selector, but it can test sibling relationships too. The examples below are ordinary CSS: place them in a stylesheet or a <style> element on a page containing the matching markup.
Syntax and matching rules
subject:has(relative-selector-list) {
/* declarations apply to subject */
}
The browser evaluates each relative selector using the subject as its anchor. If any selector in the comma-separated list matches, the subject matches. Without an explicit combinator, the relationship is to a descendant.
/* Select .card if it contains an img anywhere below it */
.card:has(img) {
border-color: steelblue;
}
/* Select .card only if an img is its direct child */
.card:has(> img) {
padding-top: 0;
}
/* Select h2 if the immediately following sibling is a p */
h2:has(+ p) {
margin-bottom: 0;
}
/* Select article if it has an img or a video descendant */
article:has(img, video) {
border-color: #567;
}
In .card:has(img), the card receives the declaration, not the image. The image is evidence that makes the card match. A selector list inside :has() is an OR: one matching alternative is enough.
Descendant, child, and sibling examples
Descendant: a form with an invalid control
form:has(:invalid) {
border: 2px solid crimson;
padding: 1rem;
}
form:has(:invalid) .form-error {
display: block;
}
The first rule matches a form when any descendant matches :invalid. The second also relies on that form-level condition, then selects a descendant error element. Provide the error element in the markup and give it a hidden baseline style if it should only appear on invalid forms:
.form-error {
display: none;
}
Direct child: cards with a top-level image
.card:has(> img) {
display: grid;
grid-template-columns: 8rem 1fr;
gap: 1rem;
}
This does not match a card whose image is nested inside another element. Use .card:has(img) if any descendant image should qualify.
Adjacent sibling: style a heading based on what follows
h2:has(+ p) {
margin-block-end: 0;
}
This selects the heading only when a paragraph is the immediately following element sibling. Whitespace and comments in the HTML do not become element siblings, but another element between the heading and paragraph does.
Later sibling: a label followed by a checked control
label:has(~ input:checked) {
color: seagreen;
}
The general sibling combinator ~ tests any later sibling under the same parent. This example requires the input to be a sibling of the label. If the input is inside the label, use a descendant relationship such as label:has(input:checked).
Combining conditions and writing readable selectors
Use commas in the argument when any condition may qualify the subject:
article:has(img, video) {
aspect-ratio: auto;
}
To require multiple conditions, chain :has() pseudo-classes:
article:has(img):has( figcaption) {
/* article has both an image and a figcaption somewhere inside */
}
Each pseudo-class must match, so this means the article has an image and also has a figcaption. Remove the space after ( for conventional formatting; it is shown here only as a reminder that whitespace inside a selector is meaningful. The production form is:
article:has(img):has(figcaption) {
/* article has both an image and a figcaption somewhere inside */
}
When you need a relationship within a single branch, combine selectors inside the relative selector:
article:has(img + figcaption) {
/* article contains an image immediately followed by a caption */
}
Prefer a selector that communicates the exact relationship. Broad conditions such as body:has(.active) can be harder to reason about than a component-scoped condition such as .menu:has(.active).
Restrictions and specificity
- No nesting:
:has()cannot appear inside another:has(). - No pseudo-elements in its argument: pseudo-elements are not valid relative selectors there.
- Argument affects specificity: the specificity of
:has()is based on the most specific selector in its argument list, similar to:is(). A seemingly simple subject selector can therefore have higher specificity than expected. - Comma alternatives are alternatives:
article:has(img, video)matches when either branch matches. It does not require both.
For example, .card:has(#featured) carries the ID-level specificity contributed by #featured. If the styling should be easy to override, consider using a class in the condition, organize layers deliberately, or use :where() around the argument selector when zeroing its specificity is appropriate:
.card:has(:where(#featured)) {
outline: 2px solid steelblue;
}
Browser support and progressive enhancement
The compatibility data consulted for this guide reports support beginning with Chrome 105, Edge 105, Safari 15.4, and Firefox 121. These are release thresholds, not a guarantee that every visitor’s browser supports the selector. Check your actual support matrix and the current compatibility table before making it a requirement.
For optional visual enhancement, keep useful baseline styling outside the feature query and layer the relational rule inside it:
.card {
border: 1px solid #bbb;
}
@supports selector(:has(*)) {
.card:has(> img) {
border-color: steelblue;
}
}
A selector containing unsupported :has() may invalidate its rule. Do not assume a comma-separated ordinary selector list will retain its supported branch when another selector is invalid. Keep fallback rules separate or guard the enhancement with @supports selector(:has(*)).
Performance and maintainability
:has() expresses relationships that previously often required extra classes or JavaScript. Keep conditions scoped to the component that needs them, and choose the narrowest useful relationship: a direct-child check is more specific in meaning than a broad descendant search. Avoid adding many complex relational selectors across very broad parts of the document without a concrete need.
There is no universal performance number for a selector independent of the page and browser. If a relational rule is on a frequently updated part of a large page, profile the real page in the browsers you support. For maintainability, name the intended state in a comment when the relationship is not obvious, and check specificity when an override stops applying.
Troubleshooting common :has() problems
| Symptom | Likely cause | Fix |
|---|---|---|
| The child is styled, but the parent is not | The selector before :has() is misunderstood; the subject is the element that receives declarations. |
Put the parent or intended subject before :has(), such as .card:has(img). |
| A nested image does not match the rule | The selector uses :has(> img), which checks only direct children. |
Use :has(img) to include descendants, or correct the markup if a direct child is required. |
| A following element does not match | + requires the immediately following element sibling; ~ checks later siblings. |
Choose the combinator that matches the markup relationship and ensure both elements share the required parent. |
| The entire rule or selector list is ignored | The browser may not support :has(), or an invalid selector has invalidated a normal selector list. |
Check the browser matrix; keep the fallback separate and guard enhancements with @supports selector(:has(*)). |
| An override does not take effect | A specific selector inside the argument raised the rule’s specificity. | Inspect the matched rule and specificity; reduce it with a class or suitable :where() usage, or adjust cascade layers. |
| The condition never matches | The argument’s relationship is evaluated from the subject as anchor, and the expected element may be outside that relationship. | Inspect the DOM and verify descendant, direct-child, or sibling structure; test a simpler selector first. |
Or skip the browser setup
If you are documenting or reviewing how a :has()-styled page renders, ScreenshotNeo can capture the result through one API request. It is a website screenshot API and MCP server; its capture options include viewport presets, custom viewport sizes, full-page capture, and waiting for a selector or network idle. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Replace the sample URL with the page you want to capture. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; those cleanup steps can be turned off. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does :has() select a parent?
It can select an ancestor based on a descendant, but it is more general than a parent selector: it can test children and following siblings too.
Can one :has() condition require two things?
Yes. Chain conditions, as in .card:has(img):has(figcaption). A comma inside one argument means either alternative may match.
Does :has() replace JavaScript?
It can handle styling that depends on DOM relationships. It does not perform application logic or change the document structure.


