|$ curl https://forge-ai.dev/api/markdown?path=docs/css/has-selector
$cat docs/css-:has()-selector.md
updated Last week·11 min read·published

CSS :has() Selector

CSSHas SelectorParent SelectorIntermediate🎯Free Tools
The Parent Selector

The :has()pseudo-class selects a parent element based on what its children contain. Often called the "parent selector," it fills a gap that CSS developers have wanted for over a decade — the ability to style a container based on its descendants, siblings, or content state.

Before :has(), you had to rely on JavaScript to detect child states and toggle classes on parent elements. Now, the browser handles it natively with zero scripting. The selector is incredibly versatile: detect empty containers, style forms based on input state, create conditional layouts, and much more.

has-selector-basics.css
CSS
1/* Style a card differently when it contains an image */
2.card:has(img) {
3 display: grid;
4 grid-template-columns: 200px 1fr;
5}
6
7/* Style a list item when it has a checked checkbox */
8.todo-item:has(input:checked) {
9 opacity: 0.5;
10 text-decoration: line-through;
11}
12
13/* Style a form group when it contains an invalid input */
14.form-group:has(input:invalid) {
15 border-color: #ef4444;
16}
Basic Syntax

The :has() function accepts a selector list — the same format you would pass to :is() or :where(). You can combine multiple selectors with commas inside the function.

has-syntax.css
CSS
1/* Single selector */
2.parent:has(.child) { /* styles */ }
3
4/* Multiple selectors (OR logic) */
5.card:has(img, video, svg) { /* styles */ }
6
7/* Compound selectors */
8.sidebar:has(> .widget.active) { /* styles */ }
9
10/* Negation inside :has() */
11.form:has(input:not(:placeholder-shown)) { /* styles */ }
12
13/* :has() combined with other pseudo-classes */
14.article:has(h2):hover { /* styles */ }

The argument inside :has() is relative to the element the pseudo-class is applied to. The : at the start of a compound selector is optional — both :has(.active) and :has(.active) work identically.

📝

note

The :has() selector has excellent browser support: Chrome 105+, Safari 15.4+, Firefox 121+. It is safe to use in production as of 2024.
Complex Selector Patterns

:has() shines when combined with other selectors. You can query for deeply nested descendants, sibling relationships, and compound conditions that would otherwise require multiple JavaScript event listeners.

has-complex.css
CSS
1/* Descendant of a descendant */
2.page:has(.sidebar .ad-banner) {
3 /* Apply layout shift when sidebar has an ad */
4 --sidebar-width: 320px;
5}
6
7/* Adjacent sibling inside a parent */
8.list:has(li + li) {
9 /* Style a list only when it has more than one item */
10 padding: 0;
11}
12
13/* :has() with combinators */
14.nav-item:has(+ .nav-item.active) {
15 /* Style the item BEFORE the active item */
16 color: #808080;
17}
18
19/* Nested :has() — parent contains an element that has a child */
20.form:has(.field:has(input:focus)) {
21 /* Form that currently has a focused field */
22 box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
23}

info

You can use :has() to detect the presence of specific media (images, videos) and adjust the parent layout accordingly — something that previously required JavaScript IntersectionObserver or ResizeObserver.
Empty State Detection

One of the most practical uses of :has() is detecting empty containers and showing placeholder content. Combined with :not(:has()), you can create elegant empty states without JavaScript DOM checks.

empty-states.css
CSS
1/* Style a container when it has NO children */
2.empty-state:empty {
3 min-height: 200px;
4 display: flex;
5 align-items: center;
6 justify-content: center;
7 border: 2px dashed #333;
8 color: #808080;
9 font-size: 14px;
10}
11
12/* Alternative using :not(:has()) */
13.list-container:not(:has(li)) {
14 min-height: 120px;
15 background: #1A1A2E;
16 border-radius: 8px;
17 display: grid;
18 place-items: center;
19}
20
21/* Show a placeholder when a section has no images */
22.gallery:not(:has(img))::before {
23 content: "No images yet";
24 color: #808080;
25 font-style: italic;
26}
preview
Card Layout Patterns

Cards often have optional elements — an image, a badge, a footer. With :has(), you can conditionally adjust the layout based on which elements are present, keeping your HTML clean and your CSS adaptive.

card-layouts.css
CSS
1.card {
2 padding: 24px;
3 background: #1A1A2E;
4 border-radius: 12px;
5 border: 1px solid #222;
6}
7
8/* Horizontal layout when card has an image */
9.card:has(img) {
10 display: grid;
11 grid-template-columns: 180px 1fr;
12 gap: 20px;
13}
14
15/* Add top padding when there is a badge */
16.card:has(.badge) {
17 padding-top: 40px;
18 position: relative;
19}
20
21/* Add bottom border when footer is present */
22.card:has(footer) {
23 border-bottom: 3px solid #00FF41;
24}
25
26/* Compact card when it has both image and badge */
27.card:has(img):has(.badge) {
28 grid-template-columns: 140px 1fr;
29 padding: 16px;
30}
card-layouts.html
HTML
1<!-- Card with image — automatic horizontal layout -->
2<div class="card">
3 <img src="photo.jpg" alt="Photo">
4 <div>
5 <h3>Card Title</h3>
6 <p>Description text</p>
7 </div>
8</div>
9
10<!-- Card without image — default vertical layout -->
11<div class="card">
12 <h3>Text Only Card</h3>
13 <p>No image, no grid — just stacked content</p>
14</div>
15
16<!-- Card with badge — shifted content down -->
17<div class="card">
18 <span class="badge">New</span>
19 <h3>Featured Card</h3>
20 <p>Badge pushes content down with extra padding</p>
21</div>
Form Validation Styling

:has() transforms form styling. Instead of relying on adjacent sibling selectors (which only work when the error message immediately follows the input), you can now style the entire form group based on its input state.

form-validation.css
CSS
1.form-group {
2 padding: 16px;
3 border: 1px solid #222;
4 border-radius: 8px;
5 transition: border-color 0.2s, background 0.2s;
6}
7
8/* Highlight when any input in the group is focused */
9.form-group:has(input:focus) {
10 border-color: #3B82F6;
11 background: rgba(59, 130, 246, 0.05);
12}
13
14/* Error state when input is invalid and not empty */
15.form-group:has(input:invalid:not(:placeholder-shown)) {
16 border-color: #ef4444;
17 background: rgba(239, 68, 68, 0.05);
18}
19
20/* Success state when input is valid */
21.form-group:has(input:valid:not(:placeholder-shown)) {
22 border-color: #22c55e;
23 background: rgba(34, 197, 94, 0.05);
24}
25
26/* Style the submit button when ALL fields are valid */
27form:has(input:valid:not(:placeholder-shown)):has(textarea:valid:not(:placeholder-shown)) button[type="submit"] {
28 background: #3B82F6;
29 cursor: pointer;
30}
31
32/* Disable button when any field is invalid */
33form:has(input:invalid:not(:placeholder-shown)) button[type="submit"] {
34 opacity: 0.5;
35 cursor: not-allowed;
36}
preview
Best Practices
Use :has() sparingly — complex :has() selectors can be expensive for the browser to evaluate
Prefer simpler :has() queries over deeply nested ones for better performance
Combine :has() with CSS custom properties for conditional theming
Use :not(:has()) to detect empty containers instead of JavaScript DOM checks
Keep :has() selectors readable — if it takes more than a second to understand, simplify
Test :has() performance with DevTools Performance panel on large DOM trees
has-best-practices.css
CSS
1/* Good — simple, readable */
2.card:has(img) {
3 display: grid;
4 grid-template-columns: 200px 1fr;
5}
6
7/* Avoid — overly complex, hard to maintain */
8.page:has(> header:has(> nav:has(.active)):has(> .banner)) .main {
9 /* This is fragile and hard to reason about */
10}
11
12/* Better — break into smaller, composable rules */
13.page:has(header nav .active) .main {
14 /* Responsive when nav is expanded */
15}
16
17.page:has(.banner) .main {
18 /* Account for banner presence */
19}
Browser Support

The :has() selector is supported in all major browsers as of 2024. It is production-ready for modern web applications.

BrowserSupportVersion
Chrome105+
Edge105+
Firefox121+
Safari15.4+
$Blueprint — Engineering Documentation·Section ID: CSS-47·Revision: 1.0