CSS Selectors & Specificity — The Complete Deep-Dive Guide
Master every CSS selector type — type, class, ID, attribute, combinators, pseudo-selectors — and understand specificity, the cascade, inheritance, and @layer for bulletproof styling.
Introduction
CSS selectors are the grammar of CSS. They determine which elements your styles target. Understanding them deeply — including how the browser decides which rule wins when two selectors target the same element — is the foundation of writing CSS that is predictable, maintainable, and conflict-free.
Most developers use 5–6 selectors daily. CSS has over 50. Mastering the full set unlocks solutions that would otherwise require JavaScript or extra HTML.
1. Basic Selectors
Universal Selector *
Matches every element in the document.
/* CSS Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Debug: visualize all elements */
* {
outline: 1px solid red;
}
Specificity: (0, 0, 0) — zero.
Type Selector
Matches all elements of a given HTML tag.
p {
line-height: 1.75;
margin-bottom: 1.5rem;
}
h1, h2, h3 {
font-family: var(--font-display);
line-height: 1.2;
}
a {
color: var(--color-primary);
text-decoration: none;
}
Specificity: (0, 0, 1)
Class Selector
The workhorse of CSS. Targets elements with a specific class attribute.
.card {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.btn-primary {
background: var(--color-primary);
color: white;
}
/* Chaining — element must have BOTH classes */
.text-lg.text-bold {
font-size: 1.25rem;
font-weight: 700;
}
Specificity: (0, 1, 0)
ID Selector
Targets a single element by its unique id. Has high specificity — use sparingly.
#hero {
min-height: 100vh;
background: linear-gradient(135deg, #667eea, #764ba2);
}
/* Good use: skip navigation for accessibility */
#skip-link {
position: absolute;
top: -100%;
left: 8px;
}
#skip-link:focus {
top: 8px;
}
Specificity: (1, 0, 0)
2. Attribute Selectors
Target elements based on their HTML attributes and values — one of the most underused selector families.
Presence [attr]
/* Any element with a disabled attribute */
[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
/* Any element with a data-tooltip attribute */
[data-tooltip] {
position: relative;
cursor: help;
}
Exact Match [attr="value"]
[type="text"] {
border: 1.5px solid #e5e7eb;
border-radius: 6px;
padding: 10px 14px;
}
[type="submit"] {
background: var(--color-primary);
color: white;
cursor: pointer;
}
Starts With [attr^="value"]
/* All external links */
a[href^="http"] {
/* external link treatment */
}
/* WhatsApp links */
a[href^="https://wa.me"]::before {
content: "💬 ";
}
Ends With [attr$="value"]
/* PDF links */
a[href$=".pdf"]::after {
content: " (PDF)";
font-size: 0.75em;
color: #ef4444;
font-weight: 600;
}
Contains [attr*="value"]
/* Any link to YouTube */
a[href*="youtube.com"]::before {
content: "▶ ";
color: #ff0000;
}
Prefix Match [attr|="value"]
Matches value or value followed by hyphen. Designed for language codes.
/* Matches lang="en", lang="en-US", lang="en-GB" */
[lang|="en"] {
font-family: 'Georgia', serif;
}
[lang|="hi"] {
font-family: 'Noto Sans Devanagari', sans-serif;
}
Case-Insensitive [attr="value" i]
/* Matches .JPG .jpg .Jpg */
a[href$=".jpg" i],
a[href$=".jpeg" i],
a[href$=".png" i] {
display: inline-block;
}
3. Combinator Selectors
Combinators express the relationship between two selectors.
Descendant (space)
Targets all matching elements inside a parent, at any nesting depth.
/* All <a> inside .content — any nesting level */
.content a {
color: var(--color-primary);
text-decoration: underline;
}
Child >
Targets only direct children — not grandchildren.
/* Only direct <li> children of .nav > ul */
.nav > ul > li {
display: inline-block;
}
/* All direct children of .grid */
.grid > * {
flex: 1 1 0;
}
Adjacent Sibling +
Targets the immediately next sibling at the same level.
/* <p> that directly follows an <h2> */
h2 + p {
font-size: 1.1rem;
color: var(--text-muted);
margin-top: 0;
}
/* Label after checkbox — no extra classes needed */
input[type="checkbox"] + label {
cursor: pointer;
margin-left: 8px;
}
General Sibling ~
Targets all matching siblings that come after the specified element.
/* All <p> elements after an <h2> at the same level */
h2 ~ p {
border-left: 3px solid var(--color-primary);
padding-left: 16px;
}
/* Pure CSS accordion — no JS needed */
input[type="checkbox"]:checked ~ .panel {
display: block;
}
4. Specificity — The Complete Picture
Specificity is the algorithm CSS uses to decide which rule wins when multiple rules target the same element. Think of it as a 3-column score: (A, B, C).
The Scoring System
| Selector | A (IDs) | B (Classes / Attrs / Pseudo-classes) | C (Elements / Pseudo-elements) |
|---|---|---|---|
* |
0 | 0 | 0 |
p |
0 | 0 | 1 |
::before |
0 | 0 | 1 |
.card |
0 | 1 | 0 |
[type="text"] |
0 | 1 | 0 |
:hover |
0 | 1 | 0 |
.card p |
0 | 1 | 1 |
.card .title |
0 | 2 | 0 |
#hero |
1 | 0 | 0 |
#hero .card:hover p |
1 | 2 | 1 |
Scores are compared left to right. One ID beats any number of classes. One class beats any number of type selectors.
Step-by-Step Calculation
/* selector: nav.site-nav > ul li a:hover */
/* Count each part: */
/* nav → element → C: 1 */
/* .site-nav → class → B: 1 */
/* ul → element → C: 1 */
/* li → element → C: 1 */
/* a → element → C: 1 */
/* :hover → pseudo-class → B: 1 */
/* Final score: (0, 2, 4) */
/* Which wins? */
/* (0, 1, 0) */
.heading { color: blue; }
/* (0, 0, 1) — loses */
h1 { color: red; }
/* Result: BLUE — class beats element */
/* (0, 0, 2) — 2 element selectors */
article p { color: red; }
/* (0, 1, 0) — 1 class selector */
.intro { color: blue; }
/* Result: BLUE — (0,1,0) beats (0,0,2) */
Specificity of Modern Pseudo-classes
/* :is() — takes specificity of its MOST SPECIFIC argument */
:is(h1, .title, #hero) { }
/* Score: (1, 0, 0) — because #hero is in the list */
/* :where() — always ZERO specificity */
:where(h1, .title, #hero) { }
/* Score: (0, 0, 0) — perfect for base/reset styles */
/* :not() — takes specificity of its argument */
:not(.active) { }
/* Score: (0, 1, 0) */
:not(#main) { }
/* Score: (1, 0, 0) */
/* :has() — takes specificity of its argument */
.card:has(img) { }
/* Score: (0, 1, 1) — .card (0,1,0) + img (0,0,1) */
5. The Cascade — Full Priority Order
Specificity is step 4 in a 5-step process. The full cascade:
1. Origin + !important
Browser !important > User !important > Author !important
2. CSS @layer order
Later declared layers win over earlier ones
3. Specificity
Higher score wins
4. Source order
Later declaration wins on a tie
5. Inheritance
Inherited value if no rule applies
Cascade Layers @layer — Modern CSS
@layer gives you explicit control over the cascade — independent of specificity.
/* Declare order — first = lowest priority */
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
}
@layer base {
a { color: blue; } /* (0,0,1) */
}
@layer utilities {
.text-red { color: red; } /* (0,1,0) */
}
/* utilities layer wins over base layer */
/* Even though base's a selector has lower specificity */
/* LAYER ORDER wins over specificity */
Adding styles to a layer later:
/* styles.css */
@layer components;
/* component-a.css */
@layer components {
.btn { background: purple; }
}
/* component-b.css */
@layer components {
.btn { background: green; } /* Wins — later in same layer */
}
Unlayered styles beat all layers:
@layer base {
p { color: blue; } /* In a layer */
}
p { color: red; } /* NOT in any layer — wins! */
6. !important — When and Why
!important overrides the entire cascade. It should be used sparingly and intentionally.
/* Forces this regardless of specificity */
.text-red {
color: red !important;
}
/* Legitimate use cases: */
/* 1. Utility classes that must always apply */
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(0,0,0,0) !important;
}
/* 2. Overriding inline styles */
/* When a 3rd-party library adds style="" you can't remove */
/* 3. Accessibility overrides */
@media (forced-colors: active) {
.btn {
border: 2px solid ButtonText !important;
}
}
7. Inheritance
Some CSS properties are inherited by child elements automatically. Others are not.
Inherited Properties (Typography-focused)
/* These propagate from parent to child by default */
color
font-family, font-size, font-weight, font-style, font-variant
line-height
letter-spacing, word-spacing
text-align, text-transform, text-indent
list-style
visibility
cursor
Non-Inherited Properties
/* These must be set explicitly on each element */
margin, padding, border, outline
width, height
background, box-shadow
display, position, top, right, bottom, left
overflow, z-index
transform, opacity, animation
Controlling Inheritance
.child {
color: inherit; /* Force inherit parent's color */
margin: initial; /* Reset to browser/spec default */
padding: unset; /* inherit if inheritable, else initial */
border: revert; /* Revert to browser's user-agent value */
display: revert-layer; /* Revert to value from lower layer */
}
/* Reset ALL properties */
.isolated {
all: initial; /* Everything reset */
all: revert; /* Everything restored to browser stylesheet */
}
8. Practical Patterns
Zebra Table — :nth-child
tr:nth-child(even) {
background: #f9fafb;
}
tr:hover {
background: #eff6ff;
}
Responsive Navigation with Sibling Combinator
/* Pure CSS mobile menu — no JavaScript */
.menu-toggle {
display: none;
}
.menu-toggle:checked ~ .nav-links {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.menu-toggle,
.menu-icon {
display: none;
}
.nav-links {
display: flex !important;
flex-direction: row;
}
}
Smart Link Decoration
/* External links — arrow icon */
a[href^="http"]:not([href*="goswamidigitalworld.com"])::after {
content: " ↗";
font-size: 0.75em;
opacity: 0.6;
}
/* PDF icon */
a[href$=".pdf"]::before {
content: "📄 ";
}
/* Email icon */
a[href^="mailto"]::before {
content: "✉️ ";
}
Avoiding Specificity Wars — BEM + @layer
@layer base {
/* Low-specificity base styles */
:where(h1, h2, h3) {
font-family: var(--font-display);
}
}
@layer components {
/* Component styles — win over base */
.hero-title {
font-family: var(--font-sans);
font-size: 4rem;
}
}
@layer utilities {
/* Utilities always win */
.font-mono {
font-family: var(--font-mono) !important;
}
}
Cheat Sheet
/* Specificity (A, B, C) */
#id → (1, 0, 0)
.class → (0, 1, 0)
[attr] → (0, 1, 0)
:pseudo-class → (0, 1, 0)
element → (0, 0, 1)
::pseudo-element → (0, 0, 1)
* → (0, 0, 0)
:where() → (0, 0, 0)
:is() → highest arg
:not() → highest arg
:has() → highest arg
/* Cascade order (lowest → highest) */
Browser defaults
Author @layer (declared order)
Author unlayered
Author !important
User !important
/* Combinators */
A B → all descendants
A > B → direct children only
A + B → immediately adjacent sibling
A ~ B → all following siblings
Conclusion
Selectors and specificity are the rules by which CSS resolves every conflict. The developers who truly master CSS are those who understand not just what selectors do, but what the cascade does when multiple rules collide. Add @layer to your workflow, use :where() for base styles, lean on combinators to reduce HTML classes, and you'll write CSS that is both powerful and predictable.