Selectors & Specificity
Master CSS selectors from element and class patterns through combinators and pseudo-classes, and learn how specificity determines which rule wins when styles conflict.
Basic Selectors
CSS selectors are the foundation of every stylesheet. Element selectors target HTML tags, class selectors use a dot prefix, ID selectors use a hash, and attribute selectors match elements by their attributes.
Choosing the right selector type affects maintainability. Classes are the workhorse for reusable styling, while IDs should be reserved for unique page landmarks or JavaScript hooks.
- Prefer classes over IDs for styling to keep specificity low
- Attribute selectors are powerful for form inputs and link states
- Avoid overly long selector chains that are hard to override
/* Element, class, ID, and attribute selectors */
p { line-height: 1.6; }
.card { border-radius: 8px; }
#site-header { position: sticky; top: 0; }
input[type="email"] { border-color: #6366f1; }
a[href^="https"] { color: #2563eb; }Combinators
Combinators define relationships between selectors. The descendant combinator (space) matches nested elements, the child combinator (>) matches direct children, and sibling combinators (+ and ~) match adjacent or general siblings.
Combinators let you scope styles to a component without adding extra classes everywhere. Use them deliberately — deep descendant selectors increase coupling and make refactors painful.
.nav > li { display: inline-block; }
.nav li + li { margin-left: 1rem; }
article p:first-of-type { font-size: 1.125rem; }Pseudo-classes
Pseudo-classes target elements in a particular state or position. Interactive pseudo-classes like :hover, :focus, and :active style user interactions. Structural pseudo-classes like :nth-child(), :first-child, and :not() select elements by their position in the DOM.
Always pair :hover styles with :focus-visible for keyboard accessibility. Use :focus-visible instead of bare :focus to avoid distracting outlines on mouse clicks while preserving them for keyboard users.
- :focus-visible improves keyboard navigation without hurting mouse UX
- :nth-child() is ideal for zebra-striping tables and lists
- :not() reduces the need for override classes
button:hover { background: #1d4ed8; }
button:focus-visible {
outline: 2px solid #f97316;
outline-offset: 2px;
}
li:nth-child(odd) { background: #f8fafc; }
input:not([type="checkbox"]) { width: 100%; }Specificity Calculation
When two rules target the same element, the browser resolves the conflict using specificity. Inline styles beat IDs, IDs beat classes, and classes beat elements. When specificity is equal, the last rule in source order wins.
Understanding specificity prevents "specificity wars" where developers keep adding !important or longer selectors. Keep specificity flat by using single-class selectors and letting source order handle intentional overrides.
- Specificity score: inline (1000), ID (100), class (10), element (1)
- Avoid !important except for utility overrides or third-party fixes
- Use :where() to zero out specificity on wrapper selectors
/* Specificity: (0, 1, 0) */
.btn { color: blue; }
/* Specificity: (0, 2, 0) — wins over .btn */
.btn.primary { color: white; }
/* Specificity: (1, 0, 0) — wins over both */
#submit { color: green; }The :is() and :where() Functions
Modern selector functions simplify complex lists. :is() takes the highest specificity among its arguments, while :where() always contributes zero specificity. Both reduce repetition when grouping selectors.
Use :where() for base styles that should be easy to override, and :is() when you need the grouped selectors to carry meaningful weight.
:where(h1, h2, h3) {
font-family: system-ui, sans-serif;
line-height: 1.2;
}
:is(.card, .panel, .widget) > header {
border-bottom: 1px solid #e2e8f0;
padding-bottom: 0.75rem;
}