Goswami Digital WorldBlog & Insights
All PostsMain SiteContact Us
← All Posts·Web Development

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.

A
Ankur Goswami
29 April 2026 · 10 min read
👁views|
❤️likes|
🔗shares
#css#selectors#specificity#cascade#frontend

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.

Next up: CSS Box Model & Display — The Complete Guide

Enjoyed this post?
...
Share this post
💬𝕏inF
← Back to BlogWork With Us →