:has() pseudo-class
The CSS :has() pseudo-class, introduced in 2022 with CSS Selectors Level 4, allows developers to style parent elements based on their child elements’ state or presence. This guide covers all :has() use cases with practical HTML and CSS examples to build interactive layouts without JavaScript.
- A:has(B) → Select A if B exists or is in a specific state
- Form & Input States
|
Use Case |
|
CSS Example |
HTML Example |
|---|---|---|---|
|
Checkbox toggle |
:has(input:checked) |
.box:has(input:checked {background:lightgreen} |
<div class="box"><input type="checkbox"></div> |
|
Unchecked state |
:has(input:not(:checked)) |
.box:has(input:not(:checked)){opacity:.5} |
<div class="box"><input type="checkbox"></div> |
|
Input focused |
:has(input:focus) |
.field:has(input:focus){border:2px solid blue} |
<div class="field"><input></div> |
|
Any focus inside |
:has(:focus-within) |
.card:has(:focus-within){outline:2px solid} |
<div class="card"><input></div> |
|
Disabled input |
:has(input:disabled) |
.row:has(input:disabled){opacity:.4} |
<div class="row"><input disabled></div> |
|
Required field |
:has(input:required) |
.field:has(input:required)::after{content:"*"} |
<div class="field"><input required></div> |
|
Valid input |
:has(input:valid) |
.group:has(input:valid){border:green} |
<div class="group"><input required></div> |
|
Invalid input |
:has(input:invalid) |
.group:has(input:invalid){border:red} |
<div class="group"><input required></div> |
|
Empty input |
:has(input:placeholder-shown) |
.wrap:has(input:placeholder-shown){opacity:.6} |
<div class="wrap"><input placeholder="Name"></div> |
|
Read-only |
:has(:read-only) |
.box:has(:read-only){background:#eee} |
<div class="box"><input readonly></div> |
- Toggle / Accordion / Tabs (No JavaScript)
| Component |
Selector |
CSS |
HTML |
|---|---|---|---|
|
Show content |
:has(:checked) |
.wrap:has(:checked) .content{display:block} |
<div class="wrap"><input type="checkbox"><div class="content"></div></div> |
|
Accordion |
:has(:checked ~ .panel) |
.item:has(:checked~.panel){height:auto} |
<div class="item"><input type="checkbox"><div class="panel"></div></div> |
|
Tabs |
:has(input[type=radio]:checked) |
.tabs:has(#t1:checked) .c1{display:block} |
<div class="tabs"><input id="t1" type="radio"></div> |
|
Details open |
:has(details[open]) |
.box:has(details[open]){border:2px solid} |
<div class="box"><details open></details></div> |
|
Modal open |
body:has(dialog[open]) |
body:has(dialog[open]){overflow:hidden} |
<dialog open></dialog> |
- Hover & Interaction Sync
| Interaction |
Selector |
CSS |
HTML |
|---|---|---|---|
| Child hover |
:has(:hover) |
.card:has(:hover){background:#f5f5f5} |
<div class="card"><button></button></div> |
|
Button hover |
:has(button:hover) |
.card:has(button:hover){transform:scale(1.05)} |
<div class="card"><button>Hover</button></div> |
|
Menu hover |
:has(li:hover) |
.menu:has(li:hover){background:#eee} |
<ul class="menu"><li>Item</li></ul> |
|
Link hover |
:has(a:hover) |
.box:has(a:hover){border-color:blue} |
<div class="box"><a>Link</a></div> |
|
Keyboard focus |
:has(:focus-visible) |
.card:has(:focus-visible){outline:2px solid} |
<div class="card"><button></button></div> |
- Navigation & Active States
|
Case |
Selector |
CSS |
HTML |
|---|---|---|---|
|
Active item |
:has(.active) |
.nav:has(.active){border-left:4px solid} |
<nav class="nav"><a class="active"></a></nav> |
|
Direct child active |
:has(> .active) |
.menu:has(>.active){background:#ddd} |
<div class="menu"><div class="active"></div></div> |
|
ARIA current |
:has([aria-current="page"]) |
.nav:has([aria-current]){font-weight:bold} |
<a aria-current="page"></a> |
|
Dropdown open |
:has([aria-expanded="true"]) |
.item:has([aria-expanded="true"]){} |
<button aria-expanded="true"></button> |
- Media Detection (Layout Logic)
| Media |
Selector |
CSS |
HTML |
|---|---|---|---|
|
Image present |
:has(img) |
.card:has(img){padding:0} |
<div class="card"><img></div> |
|
Video |
:has(video) |
.post:has(video){background:black} |
<div class="post"><video></video></div> |
|
Audio |
:has(audio) |
.post:has(audio){} |
<div class="post"><audio></audio></div> |
|
Iframe |
:has(iframe) |
.embed:has(iframe){} |
<div class="embed"><iframe></iframe></div> |
|
SVG icon |
:has(svg) |
.btn:has(svg){padding-left:40px} |
<button class="btn"><svg></svg></button> |
- Content Presence & Status
| State |
Selector |
CSS |
HTML |
|---|---|---|---|
|
Error exists |
:has(.error) |
.form:has(.error){border:red} |
<form><span class="error"></span></form> |
|
Success |
:has(.success) |
.box:has(.success){border:green} |
<div class="box"><span class="success"></span></div> |
|
Loading |
:has(.loading) |
.card:has(.loading){opacity:.5} |
<div class="card"><span class="loading"></span></div> |
|
Badge |
:has(.badge) |
.item:has(.badge){padding-right:20px} |
<div class="item"><span class="badge"></span></div> |
|
Empty content |
:has(:empty) |
.box:has(:empty){display:none} |
<div class="box"><span></span></div> |
- Lists, Tables & Data UI
|
Use |
Selector |
CSS |
HTML |
|---|---|---|---|
|
List hover |
:has(li:hover) |
.list:has(li:hover){} |
<ul class="list"><li></li></ul> |
|
Table hover |
:has(tr:hover) |
.table:has(tr:hover){} |
<table class="table"><tr></tr></table> |
|
Empty cell |
:has(td:empty) |
.row:has(td:empty){} |
<tr class="row"><td></td></tr> |
|
Table exists |
:has(table) |
.wrap:has(table){overflow:auto} |
<div class="wrap"><table></table></div> |
- Advanced Logic (Multiple Conditions)
|
Logic |
Selector |
CSS |
HTML |
|---|---|---|---|
|
Multiple states |
:has(input:checked):has(.premium) |
.card:has(input:checked):has(.premium){border:gold} |
<div class="card"><input checked><span class="premium"></span></div> |
|
Not disabled |
:has(:not(.disabled)) |
.box:has(:not(.disabled)){} |
<div class="box"><span></span></div> |
|
Structural check |
:has(> :nth-child(3)) |
.grid:has(>:nth-child(3)){} |
<div class="grid"><div></div><div></div><div></div></div> |