Posted on: 23/08/2026(updated)
For years, CSS selectors have been limited in what they can target, and often developers had to rely on long selector lists to achieve more complex specificity styling tasks.
In recent years, a suite of powerfull new CSS selectors have been introduced that are transforming how we write styles, and dramatically reducing our dependency on JavaScript.
In the past, it wasn't possible to style a parent element based on its children. Not without JavaScript.
/* Style forms inputs that contain required fields */
form:has(input:required) {
border-left: 4px solid #e74c3c;
padding-left: 1rem;
}
/* Select nav links that contains an active item */
nav:has(.active) {
background-color: #f8f9fa;
}
/* Select any article that contains an img */
article:has(img) {
display: grid;
grid-template-columns: 1fr 2fr;
}
/* Select a div that directly contains an h2 */
div:has(> h2) {
margin-bottom: 2rem;
}
Restrict matching to direct children only:
/* List items that directly contain a link */
li:has(> a) {
list-style: none;
}
/* Sections whose first child is an h2 */
section:has(> h2:first-child) {
border-top: 3px solid #0066cc;
}
Forms are one of the most impactful areas for :has(). You can style form groups, labels, and wrappers based on the state of their input descendants, and without any JavaScript.
/* Highlight a form group when its input is focused */
.form-group:has(input:focus) {
background-color: #f0f7ff;
border-left: 3px solid #0066cc;
padding-left: 1rem;
}
/* Style a form group containing an invalid input */
.form-group:has(input:invalid) {
border-left: 3px solid #cc0000;
}
/* Show helper text only when the input is focused */
.form-group:has(input:focus) .helper-text {
display: block;
}
/* Style the label when its associated input
has content (using :not(:placeholder-shown)) */
.form-group:has(input:not(:placeholder-shown)) label {
color: #0066cc;
font-size: 0.75rem;
transform: translateY(-1.5rem);
}
/* Dark mode toggle without JavaScript */
body:has(#dark-mode-toggle:checked) {
--bg: #1a1a2e;
--text: #e0e0e0;
--surface: #16213e;
background-color: var(--bg);
color: var(--text);
}
/* Accordion panel: show content
when checkbox is checked */
.accordion-item:has(input[type="checkbox"]
:checked) .accordion-content {
display: block;
max-height: 500px;
}
/* Tab system: highlight active tab */
.tabs:has(#tab-2:checked) .tab-label[for="tab-2"] {
border-bottom: 3px solid #0066cc;
color: #0066cc;
}
.tabs:has(#tab-2:checked) #panel-2 {
display: block;
}
/* Hide a section wrapper when
its content area is empty */
.section-wrapper:has(.content:empty) {
display: none;
}
/* Show a placeholder message when
a list has no items */
.todo-list:not(:has(li)) .empty-state {
display: flex;
}
.todo-list:has(li) .empty-state {
display: none;
}
Instead of doing this:
h1 > b, h2 > b, h3 > b, h4 > b, h5 > b, h6 > b {
color: red;
}
Now, you could use :is() and improve legibility, and avoid long selectors
:is(h1,h2,h3,h4,h5,h6) > b {
color: hotpink;
}
:where(h1,h2,h3,h4,h5,h6) > b {
color: hotpink;
}
Instead of doing this:
article > header,
article > #nav {
background: white;
}
We can write our styles this way:
article > :is(header, #nav) {
background: white;
}