:has() Is the Parent Selector We Waited Twenty Years For
"CSS can't select parents" was true for so long that many of us stopped questioning it. It stopped being true in every major browser by the end of 2023, and I still catch myself writing JavaScript class-toggles that :has() makes unnecessary.
The form styling everyone wants
Highlight the whole field wrapper (label included) when the input inside is invalid, focused, or filled:
.field:has(input:focus-visible) {
outline: 2px solid steelblue;
outline-offset: 2px;
}
.field:has(input:user-invalid) {
border-color: crimson;
background: #fff5f5;
}
.field:has(input:user-invalid) label {
color: crimson;
}
Note :user-invalid rather than :invalid - it waits until the user has actually interacted with the field, so forms don't load pre-covered in red.
Cards that adapt to their own content
/* Tighter padding when the card has an image on top */
.card:has(img) { padding-top: 0; }
/* A grid that switches layout when a sidebar exists */
.layout:has(> aside) {
grid-template-columns: 1fr 300px;
}
This is the real shift: components styling themselves by inspecting their own composition, instead of you maintaining .card--with-image modifier classes that drift out of sync.
Quantity queries
:has() combined with counting selectors answers "style differently when there are N items":
/* 5 or more tabs? shrink them */
.tabs:has(> :nth-child(5)) > * {
font-size: 13px;
padding-inline: 8px;
}
It's not only about parents
:has() checks any relationship. Previous-sibling selection, long "impossible", is now mundane:
/* The label BEFORE a checked radio */
label:has(+ input:checked) { font-weight: 600; }
And a page-level trick I genuinely use: dim everything except the hovered card.
.grid:has(.card:hover) .card:not(:hover) {
opacity: 0.6;
}
Practical notes
- Support: Chrome 105+, Safari 15.4+, Firefox 121+. All comfortably past.
- You cannot nest :has() inside :has(). Rarely a real limitation.
- Keep the inner selector cheap.
body:has(*)-style rules force wide invalidation; scoping to a component class keeps the engine happy.
Between :has() for state and accent-color for the controls themselves, whole categories of form JavaScript are now deletable.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.