The :has() selector is often called a "parent selector," but it is more general: it matches an element that contains (or otherwise matches) something inside it. That lets you style a container when it has a certain child, or style based on sibling relationships, without JavaScript.
Basic syntax
An element matches :has() if it has a descendant matching the inner selector:
/* Card that contains an image */
.card:has(img) {
padding: 0;
}
/* Form group with an error message */
.field:has(.error) {
border-color: red;
}
/* Section with a heading */
section:has(h2) {
border-top: 1px solid #ccc;
}
The element you style is the one that "has" the thing; the inner selector is what must exist inside it.
Direct child selector
Use > to match only direct children:
/* Card with a direct child image (not nested) */
.card:has(> img) {
padding: 0;
}
/* List item with a direct child link */
li:has(> a) {
list-style: none;
}
Styling based on form state
Combine :has() with :checked, :focus, :invalid, or :disabled:
Checked state
/* Style label when checkbox is checked */
label:has(input:checked) {
font-weight: bold;
color: var(--primary);
}
/* Highlight selected option */
.option:has(input:checked) {
background: var(--selected-bg);
border-color: var(--primary);
}
Focus state
/* Highlight form row when input is focused */
.form-row:has(input:focus) {
background: var(--focus-bg);
}
/* Show label differently when input has focus */
.field:has(input:focus) .label {
color: var(--primary);
transform: translateY(-4px);
}
Validation state
/* Show error styling when input is invalid */
.field:has(input:invalid) {
border-color: var(--error);
}
/* Show success styling when valid */
.field:has(input:valid:not(:placeholder-shown)) {
border-color: var(--success);
}
Card patterns
Card with image
.card {
padding: 1.5rem;
}
/* No padding when card has image */
.card:has(> .card-image) {
padding: 0;
}
.card:has(> .card-image) .card-content {
padding: 1.5rem;
}
Card with badge
/* Add visual indicator for featured cards */
.card:has(.featured-badge) {
border: 2px solid var(--primary);
box-shadow: 0 4px 12px rgb(var(--primary-rgb) / 0.2);
}
Empty state
/* Style container when it has no items */
.list:not(:has(.item)) {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
.list:not(:has(.item))::after {
content: 'No items found';
color: var(--muted);
}
Adjacent sibling patterns
:has() can look at following siblings with + and ~:
Heading followed by paragraph
/* Tighten spacing when heading is followed by paragraph */
h2:has(+ p) {
margin-bottom: 0.5rem;
}
/* Normal spacing when no paragraph follows */
h2:not(:has(+ p)) {
margin-bottom: 1.5rem;
}
Input with adjacent error
/* Style input when it has an error message following */
input:has(+ .error-message) {
border-color: var(--error);
background: var(--error-bg);
}
List item with sublist
/* Style list item that contains a nested list */
li:has(> ul),
li:has(> ol) {
padding-bottom: 0;
}
Navigation patterns
Active nav section
/* Style nav section containing active link */
.nav-section:has(.active) {
background: var(--active-section-bg);
}
/* Expand nav section with active item */
.nav-section:has(.active) .nav-items {
display: block;
}
Dropdown with content
/* Style dropdown trigger when dropdown has items */
.dropdown:has(.dropdown-item) .dropdown-trigger {
cursor: pointer;
}
/* Disable when empty */
.dropdown:not(:has(.dropdown-item)) .dropdown-trigger {
cursor: not-allowed;
opacity: 0.5;
}
Layout patterns
Sidebar detection
/* Adjust main content when sidebar exists */
.page:has(.sidebar) .main {
margin-left: var(--sidebar-width);
}
/* Full width when no sidebar */
.page:not(:has(.sidebar)) .main {
max-width: 100%;
}
Grid adjustments
/* Single column when only one item */
.grid:has(> :only-child) {
grid-template-columns: 1fr;
max-width: 600px;
margin: 0 auto;
}
/* Two columns when two items */
.grid:has(> :nth-child(2)):not(:has(> :nth-child(3))) {
grid-template-columns: repeat(2, 1fr);
}
Combining :has() with other selectors
:has() with :not()
/* Style paragraphs not inside articles */
p:not(article:has(&) *) {
/* This is complex; usually easier with direct selectors */
}
/* More practical: items without children */
.container:has(.item):not(:has(.item .item)) {
/* Has items but no nested items */
}
:has() with :is() and :where()
/* Match multiple conditions */
.card:has(:is(img, video, iframe)) {
aspect-ratio: 16/9;
}
/* Lower specificity with :where() */
.card:has(:where(img, video)) {
overflow: hidden;
}
Performance considerations
:has() is well-optimized in modern browsers, but avoid:
/* Too broad - matches entire document */
body:has(*) { }
/* Very complex nested :has() */
.a:has(.b:has(.c:has(.d))) { }
Keep selectors focused:
/* Good - scoped to specific elements */
.card:has(img) { }
.form-field:has(input:focus) { }
Browser support
:has() is supported in:
- Chrome 105+
- Firefox 121+
- Safari 15.4+
- Edge 105+
Use progressive enhancement:
/* Default style */
.card {
padding: 1rem;
}
/* Enhanced with :has() */
.card:has(img) {
padding: 0;
}
Browsers that do not support :has() ignore the rule and keep the default.
Common patterns summary
| Pattern | Selector |
|---|---|
| Parent has child | .parent:has(.child) |
| Parent has direct child | .parent:has(> .child) |
| Element with checked input | label:has(input:checked) |
| Element with focused input | .field:has(input:focus) |
| Element followed by sibling | h2:has(+ p) |
| Element without children | .list:not(:has(.item)) |
Summary
:has() enables parent and sibling selection in CSS:
- Match parents containing specific children
- Style based on state (checked, focus, invalid)
- Adjust layout based on content presence
- Combine with
:not()for inverse conditions - Use progressive enhancement for older browsers
The MDN :has() reference has the full specification and additional examples.
