CSS Selectors Cheat Sheet Frontend Reference Specificity Web Development

Free CSS Selectors Cheat Sheet — Interactive Online Reference

· 18 min read

CSS selectors are the most-used and most-misunderstood part of frontend development. Every developer writes them daily, yet specificity wars, over-qualified selectors, and mysterious cascade behavior still waste hours in code reviews and debugging sessions. If you have ever stared at an element wondering why your style is not applying — or worse, why a style you did not write is winning — this guide is for you.

This article is a field guide to CSS selectors. We cover every selector type you will encounter in production, explain how specificity actually works (with real scores, not vague rules), and show you how to avoid the traps that catch even senior developers. When you are done reading, bookmark the interactive CSS Selectors Cheat Sheet — it is searchable, copy-ready, and runs entirely in your browser.

What Is a CSS Selector?

A CSS selector is a pattern that tells the browser which HTML elements to style. It is the bridge between your stylesheet and your markup. The selector .btn targets every element with class="btn". The selector nav > a targets every <a> that is a direct child of a <nav>. Every rule in CSS starts with a selector.

Selectors are not just about targeting elements. They are about targeting elements efficiently, maintainably, and with predictable specificity. A selector that is too broad creates unintended side effects. A selector that is too specific becomes impossible to override without !important or increasingly desperate specificity escalation. The goal is precision without over-engineering.

How CSS Specificity Works

Specificity is the algorithm browsers use to decide which style wins when multiple rules target the same element. It is not about source order alone — though order matters as a tiebreaker. Specificity is a three-part score written as (a, b, c):

  • a — Inline styles (1 if declared in style attribute, 0 otherwise)
  • b — IDs (count of ID selectors)
  • c — Classes, attributes, and pseudo-classes (count of each)
  • d — Elements and pseudo-elements (count of each) — often included as the fourth digit

Modern specificity is often written as (a, b, c, d). The browser compares left to right. A selector with (0, 1, 0, 0) beats any number of classes, no matter how many. #header at (0, 1, 0, 0) beats .nav .menu .item at (0, 0, 3, 0). This is why IDs in CSS are so powerful — and so dangerous.

Here are real examples with their specificity scores:

.nav-link:hover {
  color: #f97316;
}

Score: (0, 0, 2, 1) — two classes (.nav-link, :hover) + one element.

/* (0, 1, 1) — one class + one element */
ul.menu > li {
  display: inline-block;
}

Score: (0, 0, 1, 2) — one class (.menu) + two elements (ul, li).

/* (0, 0, 2) — two elements */
article p {
  line-height: 1.6;
}

Score: (0, 0, 0, 2) — two elements, no classes or IDs.

/* (0, 2, 0) — two classes */
.btn.primary {
  background: #3b82f6;
}

Score: (0, 0, 2, 0) — two classes, no elements.

When two selectors have the same specificity, the one that appears later in the stylesheet wins. This is the cascade in action.

The !important Trap

!important overrides specificity entirely. It is a sledgehammer, not a scalpel. Once you introduce !important, you create an arms race. The only thing that beats !important is another !important in a more specific selector, or the same specificity appearing later.

There are exactly two justified uses for !important in production CSS: utility classes that must always apply (like .sr-only for screen-reader-only text), and overriding third-party styles you cannot modify. Every other use is a sign that your specificity architecture needs fixing.

Simple Selectors

Simple selectors are the building blocks. Master these before combining them.

Universal Selector: *

The universal selector targets every element. It is useful for CSS resets and box-sizing declarations, but expensive when used for actual styling.

/* Resets — acceptable use */
*, *::before, *::after {
  box-sizing: border-box;
}

/* Performance concern — avoid */
* {
  margin: 0;
}

Specificity: (0, 0, 0, 0). The universal selector contributes zero specificity. This makes it the weakest selector in CSS — any other rule will override it.

Element Selector: element

Targets all instances of an HTML tag. p targets every paragraph. button targets every button. Element selectors are great for base typography and form resets because they apply broadly without fighting specificity battles.

p {
  margin-bottom: 1rem;
  line-height: 1.6;
}

Specificity: (0, 0, 0, 1). One element = one point in the d column.

Class Selector: .class

The workhorse of modern CSS. Classes are reusable, composable, and carry moderate specificity. A well-designed component system is built almost entirely on classes.

.card {
  background: white;
  border-radius: 0.5rem;
  padding: 1.5rem;
}

Specificity: (0, 0, 1, 0). One class = one point in the c column.

ID Selector: #id

IDs are unique per page and carry high specificity. One ID beats any number of classes. This makes them powerful for one-off overrides and dangerous for reusable components.

#header {
  position: sticky;
  top: 0;
}

Specificity: (0, 1, 0, 0). One ID = one point in the b column. Because b is weighted higher than c, #header beats .header.nav.sticky even though the class chain has three selectors.

Best practice: use IDs for in-page anchors and JavaScript hooks. Avoid styling with IDs in your stylesheet. The specificity cost is too high for the flexibility you lose.

Chained Classes

You can chain multiple classes on the same element for stricter targeting. The element must have ALL the classes to match.

.btn.primary.large {
  font-size: 1.25rem;
}

Specificity: (0, 0, 3, 0) — three classes. This is more specific than a single class but still overrideable by other class-based rules. Chained classes are useful for state variations (.btn.disabled) but should not become a replacement for proper modifier naming.

Combinator Selectors

Combinators describe relationships between elements. They are what turn simple selectors into powerful targeting patterns.

Descendant Combinator: A B

A space between selectors means "descendant of." It targets any B that is inside A, no matter how many levels deep.

/* Selects ALL .item elements inside .grid */
.grid .item {
  background: #f3f4f6;
}
<div class="grid">
  <div class="item">Direct child — MATCHES</div>
  <div>
    <div class="item">Nested grandchild — ALSO MATCHES</div>
  </div>
</div>

Use case: styling all links inside an article, all list items inside a navigation, all cells inside a table. Be careful — descendant selectors are broad and can catch elements you did not intend.

Child Combinator: A > B

The greater-than symbol targets only DIRECT children. Nested grandchildren are excluded.

/* Selects ONLY direct children */
nav > a {
  padding: 0.5rem 1rem;
}
<nav>
  <a href="/">Home</a>       <!-- MATCHES -->
  <div class="dropdown">
    <a href="/sub">Sub</a>   <!-- DOES NOT MATCH -->
  </div>
</nav>

Use case: top-level navigation links, direct table rows, grid children. Child combinators are more precise than descendant combinators and should be preferred when you only want immediate children.

Adjacent Sibling Combinator: A + B

The plus sign targets B only if it immediately follows A. They must share the same parent, and B must be the very next sibling.

/* The paragraph immediately after h2 */
h2 + p {
  font-size: 1.125rem;
  color: #4b5563;
}
<h2>Title</h2>
<p>This paragraph matches — it is immediately after h2.</p>
<p>This one does not — there is a paragraph between.</p>

Use case: adding top margin to a paragraph that follows a heading, styling a label that follows a checked checkbox, creating spacing between list items without affecting the first one.

General Sibling Combinator: A ~ B

The tilde targets ALL B elements that follow A, as long as they share the same parent. Unlike +, they do not need to be adjacent.

/* All paragraphs after h2, not just first */
h2 ~ p {
  margin-left: 1rem;
}
<h2>Title</h2>
<p>Matches.</p>
<div>Not a paragraph.</div>
<p>Also matches — still after h2.</p>

Use case: styling all paragraphs in a section after a heading, highlighting all items after a featured item in a list.

:has() — The Parent Selector

:has() is the most significant addition to CSS in years. It lets you select an element based on its children or siblings — something previously impossible without JavaScript.

/* Cards that contain an image */
.card:has(img) {
  border: 2px solid #3b82f6;
}

/* Forms with invalid inputs */
form:has(:invalid) {
  border-left: 4px solid #ef4444;
}

Browser support: All modern browsers as of 2023. No polyfill needed for current projects. :has() opens entirely new patterns: styling parent containers based on child state, creating conditional layouts, and eliminating JavaScript hacks for simple parent-selection logic.

Specificity note: :has() itself counts as a pseudo-class, but its argument also contributes specificity. .card:has(img) has specificity (0, 0, 2, 0) — the .card class plus the :has() pseudo-class.

Attribute Selectors

Attribute selectors target elements based on their HTML attributes. They are incredibly useful for styling links, forms, and data-driven markup without adding extra classes.

Presence and Exact Match

/* Has a target attribute */
a[target] {
  color: #8b5cf6;
}

/* Exact match */
input[type="text"] {
  border: 1px solid #d1d5db;
}

Word Match: [attr~="value"]

Selects elements where the attribute value contains the specified word as a space-separated token. This is how the class attribute works internally.

/* class contains "active" as a word */
[class~="active"] {
  font-weight: 600;
}

Prefix, Suffix, and Substring

/* Starts with https:// */
a[href^="https://"] {
  background: url(/icons/external.svg) no-repeat right center;
}

/* Ends with .pdf */
a[href$=".pdf"] {
  background: url(/icons/pdf.svg) no-repeat right center;
}

/* Contains "download" anywhere */
a[href*="download"] {
  color: #10b981;
}

These are powerful for adding icons to links based on file type or protocol, styling inputs by type without separate classes, and highlighting special URLs.

Case-Insensitive Matching: i

Add i before the closing bracket for case-insensitive matching.

/* Case-insensitive match */
input[type="email" i] {
  border-color: #3b82f6;
}

Specificity: All attribute selectors count as one class-level selector in the c column. [type="text"] has the same specificity as .input.

Pseudo-Classes

Pseudo-classes target elements in a specific state, position, or condition. They are the most diverse category of selectors.

Structural Pseudo-Classes

These select elements based on their position in the DOM tree.

Selector Targets Specificity
:first-child First child of its parent (0,0,1,0)
:last-child Last child of its parent (0,0,1,0)
:only-child Element with no siblings (0,0,1,0)
:nth-child(n) Element at position matching formula (0,0,1,0)
:nth-last-child(n) Same, counting from the end (0,0,1,0)
:first-of-type First of its element type among siblings (0,0,1,0)
:last-of-type Last of its element type among siblings (0,0,1,0)
:nth-of-type(n) Element at position among same-type siblings (0,0,1,0)
:only-of-type Only element of its type among siblings (0,0,1,0)
:empty Element with no children (not even text) (0,0,1,0)

The an+b Notation

:nth-child() and :nth-of-type() accept a formula in an+b notation:

  • n represents all positive integers starting from 0
  • a is the step size
  • b is the offset
/* Every 3rd item, starting at 2 */
li:nth-child(3n + 2) {
  background: #fef3c7;
}

/* First 4 items */
li:nth-child(-n + 4) {
  font-weight: 600;
}

/* Even items */
li:nth-child(even) {
  background: #f9fafb;
}

Common keywords: even = 2n, odd = 2n+1. The formula is powerful once you internalize it. Striped tables, grid layouts, and featured item rotations all become one-line selectors.

Logical Pseudo-Classes

These selectors group conditions and reduce repetition.

:not()

Excludes elements matching the argument. :not() itself adds no specificity, but its argument does.

/* Specificity: (0,0,1,0) — the .active class */
li:not(.active) {
  opacity: 0.6;
}

:is() and :where()

Both accept a selector list and match if any argument matches. The difference is specificity:

/* :is() takes the specificity of its most specific argument */
:is(h1, h2, h3) a {
  color: #3b82f6; /* specificity: (0, 0, 2) */
}

/* :where() always has zero specificity */
:where(h1, h2, h3) a {
  color: #3b82f6; /* specificity: (0, 0, 0) */
}

Use :is() when you want the specificity to reflect what matched. Use :where() when you want zero specificity — perfect for reset stylesheets and base styles that should be easy to override.

User Action Pseudo-Classes

These respond to user interaction. They are critical for accessible, responsive interfaces.

Selector Triggers When
:hover Pointer is over the element
:focus Element has keyboard focus
:focus-visible Element has focus AND the focus should be visible
:focus-within Element OR any of its descendants have focus
:active Element is being activated (mouse down, enter pressed)
/* Visible focus only — keyboard users */
button:focus-visible {
  outline: 3px solid #3b82f6;
  outline-offset: 2px;
}

/* Container has focused element */
.search-box:focus-within {
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}

Accessibility note: :focus-visible is the modern replacement for :focus on interactive elements. It shows focus rings for keyboard users while hiding them for mouse users, eliminating the ugly outline-on-click problem without breaking accessibility.

Link and Target Pseudo-Classes

Selector Targets
:link Unvisited links
:visited Visited links (limited styling for privacy)
:any-link All links (visited or not)
:target Element whose ID matches the URL hash

:target is underused and powerful. It enables in-page navigation highlights, tab-like interfaces without JavaScript, and expandable sections triggered by URL hash.

/* Highlight the section being linked to */
section:target {
  background: #fef3c7;
  padding: 2rem;
  border-left: 4px solid #f59e0b;
}

Form State Pseudo-Classes

These selectors react to form control states, enabling rich validation UX without JavaScript.

/* Checked checkbox adjacent to label */
input:checked + label {
  text-decoration: line-through;
  color: #9ca3af;
}

/* Valid/invalid states */
input:valid {
  border-color: #10b981;
}
input:invalid {
  border-color: #ef4444;
}

/* Required field indicator */
label:has(+ input:required)::after {
  content: " *";
  color: #ef4444;
}
Selector Matches When
:checked Checkbox or radio is selected
:indeterminate Checkbox in indeterminate state
:disabled Control is disabled
:enabled Control is enabled
:required Control has required attribute
:optional Control does not have required
:valid Control passes validation
:invalid Control fails validation
:in-range Value is within min/max
:out-of-range Value is outside min/max
:placeholder-shown Placeholder text is visible
:read-only Control is not editable
:read-write Control is editable

Pseudo-Elements

Pseudo-elements create virtual elements that are not in the DOM. They style specific parts of an element or insert generated content. Modern syntax uses double colons (::) to distinguish them from pseudo-classes. Single-colon syntax (:) still works for backwards compatibility but should be avoided in new code.

Pseudo-Element Purpose Specificity
::before Inserts content before element (0,0,0,1)
::after Inserts content after element (0,0,0,1)
::first-line First line of text (0,0,0,1)
::first-letter First letter of text (0,0,0,1)
::selection User-selected text (0,0,0,1)
::marker List item bullet/number (0,0,0,1)
::placeholder Input placeholder text (0,0,0,1)
::backdrop Background behind modal dialogs (0,0,0,1)
/* Decorative elements */
.quote::before {
  content: """;
  font-size: 3rem;
  color: #d1d5db;
}

/* First letter styling */
.article::first-letter {
  font-size: 3rem;
  float: left;
  line-height: 1;
  margin-right: 0.5rem;
}

/* Selection color */
::selection {
  background: #3b82f6;
  color: white;
}

/* List markers */
li::marker {
  color: #f97316;
  font-size: 1.2em;
}

Important: ::before and ::after require a content property to appear, even if it is empty. Without content: "", the pseudo-element does not render.

Specificity Deep Dive and Common Pitfalls

Specificity Wars

A specificity war happens when two developers (or two of your own rules) keep escalating specificity to override each other. It starts innocently: you add .button, then someone overrides with .nav .button, then you come back with #header .nav .button, and eventually someone slaps on !important. The stylesheet becomes a minefield.

The fix is architectural, not tactical:

  • Use a naming convention (BEM, CUBE, or your own) that makes specificity predictable
  • Keep component styles flat — one class per element when possible
  • Use :where() for resets and base styles
  • Reserve IDs for JavaScript hooks, not styling
  • Treat !important as a code smell

Over-Qualified Selectors

An over-qualified selector includes an element name when a class alone would suffice. It increases specificity for no benefit and couples your CSS to your HTML structure.

/* Over-qualified — avoid */
div.container {
  max-width: 1200px;
}

/* Better — same specificity, more flexible */
.container {
  max-width: 1200px;
}

Both selectors target the same element, but div.container has higher specificity (0,0,1,1) vs (0,0,1,0). If you later change the div to a section, the first selector breaks. The class-only version is more flexible and easier to override.

:where() for Zero-Specificity Resets

:where() is the most powerful tool for specificity management. Any selector inside :where() contributes zero specificity.

/* Zero-specificity reset — easy to override */
:where(.reset) a,
:where(.reset) button {
  all: unset;
}

This reset removes all default styling from links and buttons inside .reset, but any subsequent class can override it without fighting specificity. It is ideal for component containers, rich text editors, and any situation where you want base styles that are trivial to override.

Inline Styles and !important

Inline styles (style="color: red") have specificity (1,0,0,0). They beat every stylesheet rule except !important. Frameworks like React and Vue often use inline styles for dynamic values, which is fine as long as you understand the specificity cost.

!important is justified in exactly two situations:

/* Justified: utility class */
.sr-only {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
}

Utility classes like .sr-only, .hidden, and .text-center should always apply regardless of context. That is their purpose. Everything else should work within normal specificity.

Performance Myth-Busting

Selectors are almost never your performance bottleneck. The browser can match millions of selectors in milliseconds. What actually slows down rendering is:

  • Expensive properties (box-shadow, filter, backdrop-filter)
  • Layout thrashing (reading layout properties then writing them in a loop)
  • Large DOM trees with frequent mutations
  • Unoptimized images and fonts

That said, bad selector habits create maintenance problems that indirectly slow teams down. A selector like header nav ul li a span is not slow — it is brittle. Prefer classes. They are faster to match, easier to read, and more resilient to markup changes.

Quick Reference Table

Here is the complete CSS selector reference in one table. Bookmark this page or open the interactive cheat sheet for a searchable, filterable version.

Selector Type Specificity Description
* Universal (0,0,0,0) All elements
element Type (0,0,0,1) All instances of element
.class Class (0,0,1,0) Elements with class
#id ID (0,1,0,0) Element with ID
[attr] Attribute (0,0,1,0) Has attribute
[attr="val"] Attribute (0,0,1,0) Exact attribute match
[attr~="val"] Attribute (0,0,1,0) Word match in attribute
[attr|="val"] Attribute (0,0,1,0) Exact or hyphen-prefixed
[attr^="val"] Attribute (0,0,1,0) Starts with
[attr$="val"] Attribute (0,0,1,0) Ends with
[attr*="val"] Attribute (0,0,1,0) Contains substring
A B Combinator Sum of A + B Descendant
A > B Combinator Sum of A + B Direct child
A + B Combinator Sum of A + B Adjacent sibling
A ~ B Combinator Sum of A + B General sibling
:has(A) Pseudo-class (0,0,1,0) + A Parent of matching element
:first-child Structural (0,0,1,0) First child of parent
:last-child Structural (0,0,1,0) Last child of parent
:nth-child(n) Structural (0,0,1,0) Child at position n
:nth-of-type(n) Structural (0,0,1,0) Same-type sibling at position n
:only-child Structural (0,0,1,0) No siblings
:empty Structural (0,0,1,0) No children or text
:not(A) Logical Specificity of A Does not match A
:is(A, B) Logical Highest argument specificity Matches any argument
:where(A, B) Logical (0,0,0,0) Matches any argument, zero specificity
:hover User action (0,0,1,0) Pointer over element
:focus User action (0,0,1,0) Element has focus
:focus-visible User action (0,0,1,0) Focus should be visible
:focus-within User action (0,0,1,0) Element or descendant focused
:active User action (0,0,1,0) Being activated
:target Link/Target (0,0,1,0) ID matches URL hash
:checked Form (0,0,1,0) Checkbox/radio selected
:disabled Form (0,0,1,0) Control disabled
:valid Form (0,0,1,0) Passes validation
:invalid Form (0,0,1,0) Fails validation
:required Form (0,0,1,0) Has required attribute
::before Pseudo-element (0,0,0,1) Generated content before
::after Pseudo-element (0,0,0,1) Generated content after
::first-line Pseudo-element (0,0,0,1) First line of text
::first-letter Pseudo-element (0,0,0,1) First letter
::selection Pseudo-element (0,0,0,1) Selected text
::marker Pseudo-element (0,0,0,1) List bullet/number
::placeholder Pseudo-element (0,0,0,1) Placeholder text

Real-World Examples

Navigation Menu with Dropdowns

<nav class="main-nav">
  <a href="/">Home</a>
  <a href="/about">About</a>
  <div class="dropdown">
    <a href="/services">Services</a>
    <a href="/consulting">Consulting</a>
  </div>
</nav>
/* Style only top-level links */
.main-nav > a {
  padding: 1rem;
  font-weight: 500;
}

/* Style dropdown links differently */
.main-nav .dropdown a {
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
}

This uses the child combinator to style only top-level links while leaving dropdown links untouched. No extra classes needed on the markup.

Card Grid with Featured Items

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

/* First card spans 2 columns */
.card-grid .card:first-child {
  grid-column: span 2;
}

/* Every 4th card is featured */
.card-grid .card:nth-child(4n) {
  border: 2px solid #f97316;
}

:first-child and :nth-child() create visual hierarchy without adding modifier classes to every card. The markup stays clean and the styling is declarative.

More Free Developer Tools

CSS selectors are just one piece of the frontend puzzle. Here are more free tools from DevToolkit to speed up your workflow:

Conclusion

CSS selectors are not just syntax — they are the logic layer of your interface. Understanding specificity, combinators, and pseudo-classes lets you write styles that are precise, maintainable, and free of !important hacks. The difference between a developer who struggles with CSS and one who masters it is often just a solid grasp of how selectors work.

Bookmark the interactive CSS Selectors Cheat Sheet for quick reference. It covers every selector in this article, lets you search by name or category, and shows specificity scores at a glance. It runs entirely in your browser — no signup, no server calls, no distractions.

And if you are building something new, browse the full DevToolkit collection. Every tool is free, client-side, and built for developers who value speed and privacy.

Found this useful? Check out our free developer tools or browse more articles.