Chapter 7: Modern CSS Selectors: :focus
, :has
, :is
, :where
Smarter ways to target elements, write accessible interactions, and clean up your CSS
There was a time when CSS selectors were… limited.
You had to nest deep, chain ugly combos, or duplicate logic across multiple selectors.
But modern CSS gives us power tools: selector functions like :is()
, :where()
, :has()
, and interaction states like :focus-visible
.
They make your CSS:
- Easier to read
- More performant
- More maintainable
- And most importantly: more accessible
Let’s break down what each one does, when to use them, and where they can help you simplify your styles.
Section 7.1: :focus
vs :focus-visible
:focus
- Fires when an element receives focus via keyboard, mouse, or script.
- Often over-fires on click, leading to weird button outlines.
:focus-visible
- Fires only when the user is using a keyboard or screen reader.
- Doesn’t trigger on mouse clicks.
button:focus-visible {
outline: 2px solid blue;
}
button:focus {
outline: none; /* suppress default */
}
Great for accessibility. Lets you show outlines only when they’re needed.
If you remove
:focus
outlines without replacing them with:focus-visible
, you’re breaking keyboard accessibility.
Section 7.2: :is()
: Write Less, Target More
:is()
lets you group multiple selectors into one:
:is(h1, h2, h3, h4) {
margin-top: 0;
}
Why it matters:
Before:
h1, h2, h3, h4 {
margin-top: 0;
}
Fine, but brittle in longer selectors.
Now:
:is(article, section) > :is(h2, h3) {
color: navy;
}
Clean and scoped.
Specificity:
:is()
takes the highest specificity of its arguments.
Section 7.3: :where()
Like :is()
, But Zero Specificity
Same usage as :is()
, but the selector never contributes specificity.
:where(h1, h2, h3) {
font-weight: 600;
}
Use it in resets and base layers to avoid specificity wars later.
:where(button) {
border: none;
font-family: inherit;
}
Section 7.4: :has()
: Parent-Based Selector (Finally!)
This one is a game changer.
:has()
lets you style a parent based on its children.
.card:has(img) {
padding: 0;
}
Only apply padding if an image is present.
form:has(:invalid) {
border: 2px solid red;
}
Highlight invalid forms without JS
Browser support: Chrome, Edge, Safari (yes) Firefox (no but coming soon)
Use it for:
- Validations
- Toggle states
- Context-aware components
- Interactions that previously required JS
Section 7.5: Real Use Cases
Button that only shows outline for keyboard users
button {
outline: none;
}
button:focus-visible {
outline: 2px dashed #333;
}
Contextual layout changes with :has()
.article:has(blockquote) {
padding-left: 2rem;
border-left: 4px solid #ccc;
}
Style form wrapper if any input is invalid
.form-group:has(input:invalid) {
background: #ffe0e0;
}
Collapse heading margin across types
:where(h1, h2, h3, h4) + * {
margin-top: 0;
}
Section 7.6: Performance and Specificity Tips
:is()
and:where()
help you avoid duplication:where()
always wins in resets or design systems:has()
can replace JavaScript for toggling states- Avoid
:has()
on very deep/nested DOM: it can be expensive to compute - Combine with container queries for powerful responsive logic
Summary
:focus-visible
makes your UI keyboard-friendly without annoying mouse users:is()
simplifies complex selector chains:where()
lets you write base styles without specificity pain:has()
unlocks parent-level logic that was previously impossible in CSS
These selectors are not just syntax sugar, they enable patterns that were once JavaScript-only.
Resources
Coming Up Next
In Chapter 8, we’ll build accessible, semantic forms with labels, ARIA, validation, and keyboard-friendly patterns: no frameworks required.