CSS Selectors Attribute and Sibling
Attribute selectors, combinators, and sibling selectors — match states and structure without extra classes when the DOM already has the signal.
- css
- selectors
- attributes
Classes aren’t the only hook. Attribute selectors target href, type, aria-*, and data-*. Sibling combinators style elements based on what comes before them in the tree. Used carefully, they reduce class noise; used carelessly, they couple CSS tightly to markup order.
Docs: MDN attribute selectors, combinators.
Attribute selectors
/* Presence */
a[download] {
font-weight: 600;
}
/* Exact match */
input[type="password"] {
letter-spacing: 0.05em;
}
/* Space-separated token (class-like) */
div[class~="featured"] {
}
/* Hyphen-separated language prefix */
[lang|="en"] {
quotes: "“" "”";
}
/* Starts with / ends with / contains */
a[href^="https://"] {
/* external-ish */
}
a[href$=".pdf"]::after {
content: " (PDF)";
font-size: 0.85em;
}
a[href*="github.com"] {
/* contains */
}
/* Case-insensitive flag */
a[href$=".PDF" i] {
}
<input type="email" name="email" required aria-invalid="true" />
input[aria-invalid="true"] {
border-color: var(--danger);
}
input:required:not(:placeholder-shown):invalid {
/* progressive enhancement patterns */
}
Prefer aria-* and native validity hooks over inventing parallel classes when the accessibility tree already exposes state.
Sibling combinators
| Combinator | Meaning |
|---|---|
A + B |
Next-sibling — B immediately after A |
A ~ B |
Subsequent-sibling — any B after A sharing parent |
/* Error text immediately after invalid field */
input[aria-invalid="true"] + .field-error {
display: block;
color: var(--danger);
}
/* Label spacing between stacked fields */
.field + .field {
margin-block-start: 1rem;
}
/* Dim all options after a selected one? usually ~ */
.step.is-done ~ .step {
opacity: 0.7;
}
<label class="field">
<span>Email</span>
<input type="email" aria-invalid="true" />
<span class="field-error">Enter a valid email</span>
</label>
There is no previous-sibling combinator. Restructure markup or use :has() when you need “parent/previous” logic.
Child and descendant
/* Direct child only */
.menu > li {
list-style: none;
}
/* Any depth */
.menu a {
text-decoration: none;
}
> avoids accidentally styling nested menus the same way as top-level items.
Specificity note
Attribute selectors count like classes (one class-level weight), not like elements. input[type="text"] is higher than input alone.
input {
border: 1px solid #ccc;
}
input[type="text"] {
border-color: #999; /* wins over bare input */
}
When to prefer a class
- Reused visual pattern not tied to one attribute
- Markup order is unstable (React reordering)
- Server-rendered vs client-rendered attributes differ
- You need a public design-system API (
.btn--primary)
Interview out-loud
“Attribute selectors match on presence or value of attributes — useful for type, aria-invalid, and file extensions. + styles the immediate next sibling; ~ styles all following siblings. Specificity matches classes. I avoid previous-sibling assumptions and use :has() or classes when structure is fragile.”
Footguns
- Styling based on order that CMS editors will rearrange.
- Case sensitivity on attribute values without the
iflag. - Over-specific chains that break when a wrapper is added.
- Using attribute selectors for performance-sensitive huge DOMs without need (usually fine; readability first).
- Forgetting
*=can false-positive substrings.
Real form pattern
<div class="field">
<label for="pw">Password</label>
<input id="pw" type="password" name="password" aria-invalid="false" />
<p class="hint" id="pw-hint">At least 12 characters.</p>
<p class="error" id="pw-error" hidden>Password too short.</p>
</div>
.field:has(input[aria-invalid="true"]) {
--field-border: var(--danger);
}
.field input {
border-color: var(--field-border, var(--border));
}
input[aria-invalid="true"] + .error,
input[aria-invalid="true"] ~ .error {
display: block;
color: var(--danger);
}
Sibling selectors keep markup lean when the error node immediately follows the control. If React portals the error elsewhere, switch to :has on a wrapper or a state class — do not force fragile DOM order.
Related
Further reading
Related guides
- :is :where :hasSelector engines that cut repetition — :is and :where for lists, specificity traps, and :has for parent/sibling state without JS.
- Accent Color and Form ControlsUse accent-color to theme native checkboxes, radios, ranges, and progress — what it paints, what it skips, and when to custom-style instead.
- Auto-fit vs Auto-fillauto-fit collapses empty tracks; auto-fill keeps them. Use both with minmax for responsive card grids without media-query soup.
- BEM Naming MethodologyBlock, Element, Modifier naming for CSS — how it limits specificity wars, what to name, and when utility or CSS modules beat strict BEM.
- blend-mode Basicsmix-blend-mode vs background-blend-mode — how pixels combine, isolation, and the stacking/readability traps in product UI.