CSS Selectors: The Complete Guide with Examples

February 11, 2026

CSS selectors are the foundation of every stylesheet. They are the patterns you write to target HTML elements and apply styles to them. Whether you are changing the color of a heading, styling every other row in a table, or building a complex interactive form, selectors are how you tell the browser which elements your rules apply to. Understanding selectors thoroughly is the single most impactful thing you can do to write better, faster, and more maintainable CSS.

This guide covers every type of CSS selector in practical depth: basic selectors, combinators, attribute selectors, pseudo-classes, pseudo-elements, specificity calculations, the cascade, performance considerations, modern selectors like :has() and :is(), common patterns for real-world UI, and naming conventions that keep stylesheets manageable at scale. Each section includes the selector syntax, HTML context, and the resulting behavior so you can see exactly what each selector does.

⚙ Quick reference: For a printable one-page overview of every selector, see our CSS Selectors Cheat Sheet. Use our CSS Beautifier to format messy stylesheets for easier reading.

Table of Contents

  1. Introduction to CSS Selectors
  2. Basic Selectors
  3. Combinator Selectors
  4. Attribute Selectors
  5. Pseudo-Classes
  6. Pseudo-Elements
  7. Specificity Rules
  8. The Cascade and Inheritance
  9. CSS Selector Performance
  10. Modern CSS Selectors
  11. Common Selector Patterns
  12. Best Practices

Introduction to CSS Selectors

A CSS selector is the part of a CSS rule that identifies which HTML elements the rule applies to. Every CSS rule has two parts: the selector and the declaration block. The selector comes first, followed by curly braces containing the property-value pairs.

/* selector     { declaration block }     */
h1             { color: #3b82f6;          }
.card          { padding: 1rem;           }
#main-nav      { display: flex;           }
a:hover        { text-decoration: none;   }

Selectors range from simple (target every paragraph) to highly specific (target the third list item inside a nav element that has a particular data attribute, but only when it is hovered). The CSS specification defines dozens of selector types, and they can be combined in nearly unlimited ways. The key to mastering selectors is understanding the categories they fall into and how specificity determines which rules win when multiple selectors target the same element.

Here is a high-level overview of the selector categories before we dive into each one:

Basic Selectors

Basic selectors are the building blocks of CSS. They are what you use most often, and understanding them is essential before moving to more advanced patterns.

Type Selector (Element Selector)

The type selector targets all elements of a given HTML tag name. It has the lowest specificity of any named selector.

/* Targets every <p> element on the page */
p {
    line-height: 1.7;
    margin-bottom: 1rem;
}

/* Targets every <h2> element */
h2 {
    font-size: 1.5rem;
    font-weight: 700;
    margin-top: 2rem;
}

/* Targets every <a> (anchor) element */
a {
    color: #3b82f6;
    text-decoration: underline;
}
<!-- HTML -->
<p>This paragraph gets line-height: 1.7</p>
<p>So does this one. Every p element is targeted.</p>
<h2>This heading gets the h2 styles</h2>
<a href="/about">This link is blue with underline</a>

Type selectors are ideal for setting baseline styles that apply globally. Most CSS resets and normalization stylesheets use type selectors extensively.

Class Selector

The class selector targets elements that have a specific class attribute. It is prefixed with a dot (.). Classes are reusable — multiple elements can share the same class, and elements can have multiple classes.

/* Targets any element with class="card" */
.card {
    background: #1a1d27;
    border-radius: 8px;
    padding: 1.5rem;
    border: 1px solid rgba(255, 255, 255, 0.08);
}

/* Targets elements with class="btn" */
.btn {
    display: inline-block;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    cursor: pointer;
}

/* Targets elements with class="btn-primary" */
.btn-primary {
    background: #3b82f6;
    color: white;
}
<!-- HTML -->
<div class="card">This is a card.</div>
<div class="card">Another card. Same styles.</div>

<button class="btn btn-primary">Submit</button>
<!-- This button has BOTH .btn and .btn-primary styles -->

Class selectors are the workhorse of CSS. They offer a good balance of specificity (higher than type selectors, lower than IDs) and reusability. The vast majority of your CSS should use class selectors.

ID Selector

The ID selector targets the single element that has a specific id attribute. It is prefixed with a hash (#). IDs must be unique within a page — only one element can have a given ID.

/* Targets the element with id="main-header" */
#main-header {
    position: sticky;
    top: 0;
    z-index: 100;
    background: #0f1117;
}

/* Targets the element with id="search-input" */
#search-input {
    width: 100%;
    padding: 0.75rem 1rem;
    font-size: 1rem;
}
<!-- HTML -->
<header id="main-header">
    <nav>...</nav>
</header>

<input type="text" id="search-input" placeholder="Search...">

ID selectors have very high specificity, which makes them hard to override without using other IDs or !important. For this reason, many style guides recommend avoiding ID selectors in CSS and reserving IDs for JavaScript hooks and anchor links. Use classes instead for styling.

Universal Selector

The universal selector (*) matches every element in the document. It has zero specificity.

/* Reset box-sizing for all elements */
*, *::before, *::after {
    box-sizing: border-box;
}

/* Remove default margins (common reset) */
* {
    margin: 0;
    padding: 0;
}

The universal selector is most commonly used in CSS resets. Beyond that, use it sparingly. Selecting every element on the page is rarely what you actually want.

Grouping Selector (Selector List)

You can apply the same styles to multiple selectors by separating them with commas. This is called a selector list or grouping selector.

/* Apply the same styles to all heading levels */
h1, h2, h3, h4, h5, h6 {
    font-family: 'Inter', sans-serif;
    line-height: 1.3;
    color: #e4e4e7;
}

/* Multiple classes sharing styles */
.btn-primary, .btn-secondary, .btn-danger {
    display: inline-block;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    border: none;
    cursor: pointer;
    font-size: 0.9rem;
}

One important caveat: in traditional selector lists, if any one selector in the list is invalid, the entire rule is discarded. The modern :is() selector (covered later) fixes this problem by making invalid selectors forgiving.

Combinator Selectors

Combinators define the relationship between two selectors. They let you target elements based on their position relative to other elements in the DOM tree.

Descendant Combinator (Space)

The descendant combinator selects elements that are nested inside another element at any depth. It is written as a space between two selectors.

/* Targets every <a> inside a <nav>, at any nesting depth */
nav a {
    color: #e4e4e7;
    text-decoration: none;
    padding: 0.5rem 1rem;
}

/* Targets every <p> inside an element with class="article" */
.article p {
    line-height: 1.8;
    margin-bottom: 1.25rem;
}
<!-- HTML -->
<nav>
    <a href="/">Home</a>                    <!-- matched -->
    <div>
        <a href="/about">About</a>          <!-- also matched (nested deeper) -->
    </div>
</nav>
<a href="/external">External</a>            <!-- NOT matched (not inside nav) -->

The descendant combinator is the most commonly used combinator. However, because it matches at any depth, it can select elements you did not intend. For tighter control, use the child combinator instead.

Child Combinator (>)

The child combinator selects only direct children of an element, not deeper descendants.

/* Only targets <li> elements that are direct children of <ul> */
ul > li {
    list-style: none;
    padding: 0.5rem 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

/* Only direct children of .nav-links */
.nav-links > a {
    font-weight: 500;
}
<!-- HTML -->
<ul>
    <li>Direct child — matched</li>
    <li>
        Direct child — matched
        <ul>
            <li>Nested li — NOT matched by ul > li</li>
        </ul>
    </li>
</ul>

The child combinator is essential when you have nested structures (like nested lists or nested menus) and need styles to apply only to one level.

Adjacent Sibling Combinator (+)

The adjacent sibling combinator selects an element that immediately follows another element, where both share the same parent.

/* Target the paragraph immediately after an h2 */
h2 + p {
    font-size: 1.1rem;
    color: #9ca3af;
    margin-top: 0.5rem;
}

/* Target an input immediately after a label */
label + input {
    margin-top: 0.25rem;
}
<!-- HTML -->
<h2>Section Title</h2>
<p>This paragraph is matched (immediately follows h2)</p>
<p>This paragraph is NOT matched (follows a p, not an h2)</p>

This combinator is particularly useful for controlling spacing between specific element pairs. A common use case is removing the top margin from the first paragraph after a heading, or adding extra space between certain sibling elements.

General Sibling Combinator (~)

The general sibling combinator selects all siblings that follow a specified element, not just the immediately adjacent one.

/* Target all <p> elements that come after an <h2> (as siblings) */
h2 ~ p {
    margin-left: 1rem;
}

/* All list items after .active */
.active ~ li {
    opacity: 0.5;
}
<!-- HTML -->
<h2>Title</h2>
<p>Matched (sibling after h2)</p>
<div>Not a p, so not matched</div>
<p>Also matched (still a sibling p after the h2)</p>

The general sibling combinator is less commonly used than the adjacent sibling, but it is valuable when you need to affect all following siblings — for example, dimming all items after the active item in a list.

Attribute Selectors

Attribute selectors target elements based on the presence or value of their HTML attributes. They are enclosed in square brackets and offer powerful pattern-matching capabilities.

[attr] — Has Attribute

/* Target all elements that have a "title" attribute */
[title] {
    border-bottom: 1px dotted #9ca3af;
    cursor: help;
}

/* Target all elements with a "data-tooltip" attribute */
[data-tooltip] {
    position: relative;
}
<!-- HTML -->
<span title="More info">Hover me</span>     <!-- matched -->
<span>No title attribute</span>               <!-- NOT matched -->
<div data-tooltip="Help text">Info</div>     <!-- matched by [data-tooltip] -->

[attr=value] — Exact Match

/* Target inputs of type "text" */
input[type="text"] {
    border: 1px solid #2a2e3a;
    padding: 0.5rem;
    border-radius: 4px;
}

/* Target links that open in a new tab */
a[target="_blank"] {
    padding-right: 1rem;
    background: url('external-icon.svg') no-repeat right center;
}
<!-- HTML -->
<input type="text" placeholder="Name">       <!-- matched -->
<input type="email" placeholder="Email">     <!-- NOT matched -->
<a href="/page" target="_blank">New tab</a>  <!-- matched -->

[attr^=value] — Starts With

/* Target links that start with "https://" */
a[href^="https://"] {
    color: #22c55e;
}

/* Target all classes that start with "btn-" */
[class^="btn-"] {
    display: inline-block;
    cursor: pointer;
}
<!-- HTML -->
<a href="https://example.com">Secure link</a>   <!-- matched -->
<a href="http://example.com">Insecure</a>       <!-- NOT matched -->

[attr$=value] — Ends With

/* Target links that point to PDF files */
a[href$=".pdf"] {
    color: #ef4444;
    padding-left: 1.25rem;
    background: url('pdf-icon.svg') no-repeat left center;
}

/* Target links to image files */
a[href$=".jpg"], a[href$=".png"], a[href$=".webp"] {
    border-bottom: 2px solid #3b82f6;
}
<!-- HTML -->
<a href="/docs/report.pdf">Download Report</a>     <!-- matched -->
<a href="/docs/report.docx">Word Version</a>       <!-- NOT matched -->

[attr*=value] — Contains

/* Target links that contain "devtoolbox" anywhere in href */
a[href*="devtoolbox"] {
    font-weight: 600;
}

/* Target elements with "error" anywhere in their class */
[class*="error"] {
    color: #ef4444;
    border-color: #ef4444;
}

[attr~=value] — Contains Word (Space-Separated)

/* Target elements where class contains the word "featured" */
[class~="featured"] {
    border: 2px solid #f59e0b;
}

/* This matches class="card featured" but NOT class="card featured-post" */

[attr|=value] — Starts With (Hyphen-Separated)

/* Target elements with lang starting with "en" (en, en-US, en-GB) */
[lang|="en"] {
    quotes: '\201C' '\201D' '\2018' '\2019';
}

Case-Insensitive Matching

/* Add the "i" flag for case-insensitive matching */
a[href$=".PDF" i] {
    color: #ef4444;
}
/* Matches .pdf, .PDF, .Pdf, etc. */

Attribute selectors are invaluable for styling forms, links, and data-attribute-driven components. They let you create styles that respond to the actual content and structure of your HTML without adding extra classes.

⚙ Format your CSS: When working with complex selectors, properly formatted CSS is crucial. Use our CSS Beautifier to format compressed stylesheets, or the CSS Minifier to compress them for production.

Pseudo-Classes

Pseudo-classes target elements based on their state, position in the DOM tree, or other dynamic conditions that cannot be expressed with simple selectors. They are prefixed with a single colon (:).

Interactive State Pseudo-Classes

/* :hover — when the mouse is over the element */
.btn:hover {
    background: #2563eb;
    transform: translateY(-1px);
    transition: all 0.2s ease;
}

/* :focus — when the element has keyboard focus */
input:focus {
    outline: 2px solid #3b82f6;
    outline-offset: 2px;
    border-color: #3b82f6;
}

/* :focus-visible — only when focus is from keyboard navigation */
button:focus-visible {
    outline: 2px solid #3b82f6;
    outline-offset: 2px;
}
/* Unlike :focus, this won't show the outline on mouse clicks */

/* :active — while the element is being clicked */
.btn:active {
    transform: translateY(0);
    background: #1d4ed8;
}

/* :visited — links that the user has already visited */
a:visited {
    color: #8b5cf6;
}
<!-- HTML -->
<button class="btn">Hover and click me</button>
<input type="text" placeholder="Tab to focus me">
<a href="/visited-page">I might be purple if visited</a>

The :focus-visible pseudo-class is a modern best practice. It shows focus indicators only for keyboard navigation, not mouse clicks, giving you accessible focus styles without visual clutter for mouse users.

Structural Pseudo-Classes

/* :first-child — the first child of its parent */
li:first-child {
    font-weight: 700;
    border-top: none;
}

/* :last-child — the last child of its parent */
li:last-child {
    border-bottom: none;
}

/* :nth-child(n) — the nth child of its parent */
tr:nth-child(odd) {
    background: rgba(255, 255, 255, 0.02);
}
tr:nth-child(even) {
    background: rgba(255, 255, 255, 0.05);
}

/* :nth-child with formulas */
li:nth-child(3n) {
    color: #3b82f6;     /* Every 3rd item: 3, 6, 9, 12... */
}
li:nth-child(3n+1) {
    color: #22c55e;     /* Items 1, 4, 7, 10... */
}
li:nth-child(-n+3) {
    font-weight: 700;   /* Only the first 3 items */
}
li:nth-child(n+4) {
    opacity: 0.7;       /* Everything from the 4th item onward */
}
<!-- HTML -->
<ul>
    <li>First (matched by :first-child, bold)</li>
    <li>Second</li>
    <li>Third (matched by :nth-child(3n), blue)</li>
    <li>Fourth</li>
    <li>Fifth (matched by :last-child, no bottom border)</li>
</ul>

More Structural Selectors

/* :nth-last-child — counts from the end */
li:nth-last-child(2) {
    color: #f59e0b;     /* Second-to-last item */
}

/* :only-child — element that is the only child of its parent */
p:only-child {
    font-size: 1.1rem;
}

/* :nth-of-type — like nth-child but only counts matching elements */
p:nth-of-type(2) {
    font-style: italic;  /* The second <p>, ignoring non-p siblings */
}

/* :first-of-type / :last-of-type */
h2:first-of-type {
    margin-top: 0;       /* The first h2, regardless of what's before it */
}

/* :empty — elements with no children and no text */
.container:empty {
    display: none;
}
/* Matches <div class="container"></div> but not <div class="container"> </div> (has whitespace) */

The distinction between :nth-child() and :nth-of-type() is important. :nth-child(2) selects the second child element regardless of type. :nth-of-type(2) selects the second element of that specific type. When your parent contains mixed element types, :nth-of-type() is usually what you want.

The :not() Pseudo-Class

/* Target all buttons except .disabled */
button:not(.disabled) {
    cursor: pointer;
}

/* Target all inputs except checkboxes and radios */
input:not([type="checkbox"]):not([type="radio"]) {
    width: 100%;
    padding: 0.5rem;
}

/* Target all list items except the last one */
li:not(:last-child) {
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
    margin-bottom: 0.5rem;
    padding-bottom: 0.5rem;
}

/* Target all links that are not internal */
a:not([href^="/"]):not([href^="#"]) {
    color: #22c55e;
    /* External links get a different color */
}

The :not() pseudo-class is one of the most useful selectors in modern CSS. The CSS Selectors Level 4 specification allows :not() to accept complex selectors and selector lists, not just simple selectors. All modern browsers support this.

Form State Pseudo-Classes

/* :checked — checked checkboxes and radio buttons */
input[type="checkbox"]:checked + label {
    color: #22c55e;
    text-decoration: line-through;
}

/* :disabled and :enabled */
input:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background: #1a1d27;
}

input:enabled {
    background: #0f1117;
}

/* :required and :optional */
input:required {
    border-left: 3px solid #3b82f6;
}

/* :valid and :invalid — based on HTML5 validation */
input:valid {
    border-color: #22c55e;
}

input:invalid {
    border-color: #ef4444;
}

/* :placeholder-shown — when the placeholder is visible */
input:placeholder-shown {
    font-style: italic;
    color: #6b7280;
}

/* Float label pattern: move label up when input has content */
input:not(:placeholder-shown) + label {
    transform: translateY(-1.5rem);
    font-size: 0.75rem;
    color: #3b82f6;
}
<!-- HTML -->
<form>
    <input type="email" required placeholder="Email">
    <!-- Shows blue left border (:required) -->
    <!-- Shows red border when invalid email is typed (:invalid) -->
    <!-- Shows green border when valid email is typed (:valid) -->

    <input type="checkbox" id="agree">
    <label for="agree">I agree to the terms</label>
    <!-- Label turns green with line-through when checked -->
</form>

Pseudo-Elements

Pseudo-elements target specific parts of an element rather than the element itself. They are prefixed with a double colon (::), though single-colon syntax still works for backward compatibility with older pseudo-elements.

::before and ::after

These create virtual elements as the first or last child of the selected element. They are widely used for decorative content, icons, and layout hacks.

/* Add a decorative bullet before each list item */
.custom-list li::before {
    content: "\2022";    /* Unicode bullet character */
    color: #3b82f6;
    font-weight: 700;
    margin-right: 0.5rem;
}

/* Add "(external)" after external links */
a[href^="http"]::after {
    content: " \2197";   /* Unicode arrow */
    font-size: 0.8em;
    vertical-align: super;
}

/* Tooltip using ::after */
[data-tooltip]:hover::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    background: #1a1d27;
    color: #e4e4e7;
    padding: 0.5rem 0.75rem;
    border-radius: 4px;
    font-size: 0.85rem;
    white-space: nowrap;
    z-index: 10;
}

/* Required field asterisk */
.required-label::after {
    content: " *";
    color: #ef4444;
}
<!-- HTML -->
<ul class="custom-list">
    <li>Blue bullet before this text</li>
    <li>And before this text too</li>
</ul>

<a href="https://example.com">External link gets an arrow</a>

<span data-tooltip="This is the tooltip text">Hover me</span>

<label class="required-label">Email</label>
<!-- Displays as: Email * -->

Important: ::before and ::after require the content property, even if you set it to an empty string. Without content, the pseudo-element does not render. Also, they do not work on replaced elements like <img>, <input>, or <br>.

::first-line and ::first-letter

/* Style the first line of a paragraph */
.article p::first-line {
    font-weight: 600;
    color: #e4e4e7;
}

/* Drop cap — large first letter */
.article p:first-of-type::first-letter {
    float: left;
    font-size: 3rem;
    line-height: 1;
    font-weight: 700;
    color: #3b82f6;
    margin-right: 0.5rem;
    margin-top: 0.1rem;
}
<!-- HTML -->
<div class="article">
    <p>The first letter of this paragraph will be a large blue
    drop cap. The first line will be bold and lighter colored.
    As the viewport width changes, which text is on the first
    line changes dynamically.</p>
</div>

The ::first-line pseudo-element is dynamic — it recalculates which content is on the first line as the element resizes. Only a subset of CSS properties can be applied to ::first-line and ::first-letter (mainly font, color, background, and text decoration properties).

::marker

/* Style the bullet/number of list items */
li::marker {
    color: #3b82f6;
    font-weight: 700;
    font-size: 1.1em;
}

/* Numbered list with custom marker color */
ol li::marker {
    color: #f59e0b;
}
<!-- HTML -->
<ul>
    <li>Blue bold bullet</li>
    <li>Blue bold bullet</li>
</ul>
<ol>
    <li>Amber number 1</li>
    <li>Amber number 2</li>
</ol>

The ::marker pseudo-element is the clean way to style list bullets and numbers without resorting to list-style: none and ::before hacks. It supports a limited set of properties: color, font properties, content, and a few others.

::selection

/* Style highlighted/selected text */
::selection {
    background: #3b82f6;
    color: white;
}

/* Different selection color for code blocks */
pre::selection, code::selection {
    background: #1e3a5f;
}

The ::selection pseudo-element controls how highlighted text looks. It is commonly used to match the selection color with the site's brand colors. Only a few properties work: color, background-color, text-decoration, and text-shadow.

::placeholder

/* Style input placeholder text */
input::placeholder {
    color: #6b7280;
    font-style: italic;
    opacity: 1;   /* Firefox sets a lower default opacity */
}

Specificity Rules

Specificity is how the browser decides which CSS rule wins when multiple rules target the same element with conflicting declarations. It is the single most important concept for understanding why your CSS sometimes does not work as expected.

How Specificity Is Calculated

Specificity is calculated as a three-part value, often written as (A, B, C):

The universal selector (*), combinators (>, +, ~, space), and the :where() pseudo-class contribute zero to specificity.

/* Specificity calculations: */

p                          /* (0, 0, 1) */
.card                      /* (0, 1, 0) */
#header                    /* (1, 0, 0) */
p.intro                    /* (0, 1, 1) */
div#header.active          /* (1, 1, 1) */
#nav .list li.active a     /* (1, 2, 2) */
a:hover                    /* (0, 1, 1) */
li:nth-child(2)            /* (0, 1, 1) */
::before                   /* (0, 0, 1) */
*                          /* (0, 0, 0) */
:where(.card)              /* (0, 0, 0) — :where() always contributes zero */
:is(.card)                 /* (0, 1, 0) — :is() takes the highest specificity argument */

Specificity Comparison

Specificity values are compared left to right. A higher A value always beats any combination of B and C values. A higher B value always beats any number of C values.

/* Example: which rule wins? */

/* Rule 1: specificity (0, 1, 1) */
p.intro {
    color: blue;
}

/* Rule 2: specificity (0, 0, 3) */
body div p {
    color: red;
}

/* Result: Rule 1 wins (0,1,1) beats (0,0,3)
   because B=1 beats B=0, regardless of C values */
/* Another example */

/* Rule 1: specificity (1, 0, 0) */
#title {
    font-size: 2rem;
}

/* Rule 2: specificity (0, 10, 0) — ten classes! */
.a.b.c.d.e.f.g.h.i.j {
    font-size: 1rem;
}

/* Result: Rule 1 wins. One ID beats any number of classes.
   Specificity does NOT add up like a regular number. */

Specificity Wars and How to Avoid Them

Specificity wars happen when developers keep adding more specific selectors to override each other, leading to an arms race of IDs and !important declarations. This is the most common cause of unmaintainable CSS.

/* The war begins... */
.btn { color: blue; }                    /* (0,1,0) */
div .btn { color: red; }                 /* (0,1,1) — override! */
div.container .btn { color: green; }     /* (0,2,1) — escalation */
#sidebar .btn { color: purple; }         /* (1,1,0) — nuclear option */
#sidebar .btn { color: orange !important; } /* !important — doomsday */

/* DO NOT DO THIS. Instead, keep specificity flat: */
.btn { color: blue; }
.btn--danger { color: red; }
.btn--success { color: green; }

The !important declaration overrides all specificity calculations. It should be used only as a last resort — typically for utility classes or to override third-party styles you cannot control. If you find yourself using !important frequently, your selector architecture needs refactoring.

Selector Specificity Notes
* (0, 0, 0) Lowest possible specificity
p (0, 0, 1) One type selector
.card (0, 1, 0) One class selector
#header (1, 0, 0) One ID selector
nav ul li a.active (0, 1, 4) 1 class + 4 types
#nav .list li:hover (1, 2, 1) 1 ID + 1 class + 1 pseudo-class + 1 type
style="" (inline) Beats all selectors Only !important in a stylesheet can override
!important Beats everything Use as last resort only

The Cascade and Inheritance

The cascade is the algorithm that determines which CSS rule applies when multiple rules target the same element. Understanding the cascade means understanding the order of priority the browser uses to resolve conflicts.

Cascade Priority (Highest to Lowest)

  1. Importance!important declarations beat normal declarations
  2. Origin — author styles beat user styles beat browser default styles
  3. Specificity — higher specificity beats lower specificity
  4. Source order — when specificity is equal, the last rule in the source code wins
/* Source order example: both rules have identical specificity (0,1,0) */

.title {
    color: blue;
}

.title {
    color: red;    /* This wins because it comes later in the source */
}

Cascade Layers (@layer)

CSS Cascade Layers, introduced with @layer, give you explicit control over the cascade order. Styles in later layers override styles in earlier layers, regardless of specificity.

/* Define layer order */
@layer reset, base, components, utilities;

@layer reset {
    * { margin: 0; padding: 0; box-sizing: border-box; }
}

@layer base {
    h1 { font-size: 2rem; color: #e4e4e7; }
    p  { line-height: 1.7; }
}

@layer components {
    .card { padding: 1.5rem; background: #1a1d27; }
    .btn  { padding: 0.5rem 1rem; border-radius: 4px; }
}

@layer utilities {
    .text-center { text-align: center; }
    .mt-4 { margin-top: 1rem; }
}

/* Utilities layer comes last, so .text-center overrides
   any text-align from components, even with lower specificity */

Cascade layers solve the specificity wars problem at the architectural level. By defining a layer order, you guarantee that utility classes always override component styles, which always override base styles, without needing higher specificity or !important.

Inheritance

Some CSS properties are inherited by default — child elements automatically receive the value from their parent. Others are not inherited and must be set explicitly on each element.

/* Inherited properties (set on parent, children inherit): */
body {
    font-family: 'Inter', sans-serif;  /* inherited */
    color: #e4e4e7;                    /* inherited */
    line-height: 1.6;                  /* inherited */
    font-size: 1rem;                   /* inherited */
    letter-spacing: 0.01em;            /* inherited */
}
/* All text inside <body> inherits these values automatically */

/* NOT inherited (must be set on each element): */
.parent {
    border: 1px solid red;    /* NOT inherited */
    padding: 1rem;            /* NOT inherited */
    margin: 1rem;             /* NOT inherited */
    background: #1a1d27;      /* NOT inherited */
}
/* Children do not get borders, padding, margin, or background from parent */
/* Controlling inheritance explicitly */

.child {
    color: inherit;           /* Force inheritance (use parent's value) */
    border: inherit;          /* Force a non-inherited property to inherit */
    padding: initial;         /* Reset to the property's default value */
    margin: unset;            /* Revert to inherited value if inheritable,
                                 or initial value if not */
    all: unset;               /* Reset ALL properties on this element */
}

Common inherited properties include: color, font-family, font-size, font-weight, line-height, letter-spacing, text-align, visibility, cursor, and list-style. Non-inherited properties include: background, border, margin, padding, width, height, display, position, and overflow.

CSS Selector Performance

Browsers evaluate selectors from right to left. When the browser encounters nav ul li a, it first finds all <a> elements, then checks if each one is inside an <li>, then an <ul>, then a <nav>. Understanding this right-to-left evaluation helps you write selectors that the browser can match efficiently.

Fast Selectors

/* ID selector — fastest possible */
#header { }

/* Class selector — very fast */
.card { }

/* Type selector — fast */
p { }

/* Single attribute selector — fast */
[type="text"] { }

/* Direct child — fast (short chain) */
.nav > a { }

Slower Selectors

/* Deep descendant chains — browser must check many ancestors */
body div.container ul li a span { }

/* Universal selector as the key selector (rightmost) */
.sidebar * { }   /* Browser checks EVERY element, then walks up to .sidebar */

/* Attribute selectors with substring matching on large DOMs */
[class*="btn"] { }

/* Complex :not() with many arguments */
:not(.a):not(.b):not(.c):not(.d) { }

Practical Performance Advice

In practice, CSS selector performance is a non-issue for most websites. Modern browsers can match thousands of selectors against thousands of DOM elements in milliseconds. The real performance costs come from expensive properties (box-shadow, filter, backdrop-filter), forced reflows from JavaScript, and unnecessary repaints — not from selector matching.

📚 Related: For a deep dive into CSS performance optimization beyond selectors, read our CSS Performance Optimization Guide. Use our CSS Minifier to reduce file size for production.

Modern CSS Selectors

CSS Selectors Level 4 introduced several powerful new pseudo-classes that simplify complex selection patterns. These are fully supported in all modern browsers as of 2024-2025.

:has() — The Parent Selector

The :has() pseudo-class is the most anticipated CSS feature in years. It selects an element based on what it contains. This is effectively a "parent selector" — something CSS never had before.

/* Select a card that contains an image */
.card:has(img) {
    padding: 0;
    overflow: hidden;
}

/* Select a card that does NOT contain an image */
.card:not(:has(img)) {
    padding: 1.5rem;
}

/* Style a form group when its input is focused */
.form-group:has(input:focus) {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

/* Style a label when its sibling checkbox is checked */
.form-group:has(input:checked) label {
    color: #22c55e;
    font-weight: 600;
}

/* Select a section that has a heading followed by a paragraph */
section:has(h2 + p) {
    padding: 2rem;
}

/* Style a figure differently when it contains a figcaption */
figure:has(figcaption) {
    margin-bottom: 2rem;
}
figure:not(:has(figcaption)) {
    margin-bottom: 1rem;
}
<!-- HTML -->
<div class="card">
    <img src="photo.jpg" alt="Photo">
    <h3>Card with image — no padding, overflow hidden</h3>
</div>

<div class="card">
    <h3>Card without image — has 1.5rem padding</h3>
    <p>Just text content.</p>
</div>

<div class="form-group">
    <label>Email</label>
    <input type="email">
    <!-- When this input is focused, the entire .form-group gets a blue border -->
</div>

The :has() selector eliminates entire categories of JavaScript. Form validation states, conditional layouts, responsive component variations — all of these can now be done in pure CSS.

:is() — Matches-Any Selector

The :is() pseudo-class takes a selector list and matches any element that matches at least one of the selectors in the list. It reduces repetition dramatically and is a forgiving selector (invalid selectors in the list are ignored rather than invalidating the entire rule).

/* Without :is() — repetitive */
header a:hover,
nav a:hover,
footer a:hover {
    color: #3b82f6;
    text-decoration: underline;
}

/* With :is() — concise */
:is(header, nav, footer) a:hover {
    color: #3b82f6;
    text-decoration: underline;
}

/* Targeting multiple heading levels */
:is(h1, h2, h3, h4) {
    line-height: 1.3;
    color: #e4e4e7;
}

/* Nested list styling */
:is(ol, ul) :is(ol, ul) {
    margin-left: 1.5rem;
    font-size: 0.95em;
}

/* Complex example: style links in any article-like container */
:is(article, .post, .entry, .blog-content) :is(a, a:visited) {
    color: #3b82f6;
    text-decoration: underline;
    text-underline-offset: 2px;
}

Important: :is() takes the specificity of its most specific argument. So :is(.card, #header) has the specificity of #header (1,0,0), even when it is matching .card. If you want zero specificity, use :where() instead.

:where() — Zero-Specificity :is()

The :where() pseudo-class works identically to :is(), but its specificity is always zero. This makes it perfect for default styles that should be easy to override.

/* Default styles that are trivially overridable */
:where(h1, h2, h3, h4, h5, h6) {
    margin-top: 1.5em;
    margin-bottom: 0.5em;
    line-height: 1.3;
}

/* Specificity: (0, 0, 0) — any class or even type selector overrides this */
.title {
    margin-top: 0;   /* (0, 1, 0) — easily wins over :where() */
}

/* Perfect for CSS reset/base layers */
:where(ul, ol) {
    padding-left: 1.5rem;
}

:where(a) {
    color: #3b82f6;
    text-decoration: underline;
}

/* Any component style will override these without specificity battles */

Use :where() for resets, default styles, and library/framework CSS where you want consumers to easily override your styles. Use :is() when you want the specificity to contribute normally.

:not() Level 4 — Complex Negation

The Level 4 version of :not() accepts complex selectors and selector lists, making it far more powerful than the original Level 3 version that only accepted simple selectors.

/* Level 3: only simple selectors (one at a time) */
li:not(.active):not(.disabled) { }

/* Level 4: selector lists (all modern browsers) */
li:not(.active, .disabled) {
    opacity: 0.7;
}

/* Complex selectors inside :not() */
a:not([href^="http"]) {
    /* Internal links only (no external http/https) */
    font-weight: 500;
}

/* Exclude multiple patterns at once */
input:not([type="hidden"], [type="submit"], [type="button"]) {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #2a2e3a;
}

:nth-child() of S Syntax

The newest addition to structural pseudo-classes: :nth-child() can now filter by a selector before applying the nth logic.

/* Select every other VISIBLE item (ignoring hidden ones) */
li:nth-child(odd of :not(.hidden)) {
    background: rgba(255, 255, 255, 0.03);
}

/* Select the first 3 items that have the .active class */
.item:nth-child(-n+3 of .active) {
    font-weight: 700;
    border-left: 3px solid #3b82f6;
}
<!-- HTML -->
<ul>
    <li class="hidden">Hidden item (ignored by the count)</li>
    <li>Visible odd item — gets background</li>
    <li>Visible even item</li>
    <li>Visible odd item — gets background</li>
</ul>

This "of S" syntax is incredibly useful for tables and lists where some rows are filtered or hidden. Previously, alternating row colors would break when rows were hidden. Now you can target "every odd visible row" directly in CSS.

Common Selector Patterns

Here are practical selector patterns you will use repeatedly in real projects.

Styling Forms

/* Style all text-like inputs uniformly */
input:is([type="text"], [type="email"], [type="password"],
         [type="search"], [type="url"], [type="tel"],
         [type="number"]),
textarea,
select {
    width: 100%;
    padding: 0.625rem 0.75rem;
    border: 1px solid #2a2e3a;
    border-radius: 6px;
    background: #0f1117;
    color: #e4e4e7;
    font-size: 0.95rem;
    transition: border-color 0.2s;
}

/* Focus state for all form inputs */
:is(input, textarea, select):focus {
    border-color: #3b82f6;
    outline: 2px solid rgba(59, 130, 246, 0.2);
    outline-offset: -1px;
}

/* Error state */
:is(input, textarea, select):user-invalid {
    border-color: #ef4444;
}

/* Disabled state */
:is(input, textarea, select):disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Label above required fields */
label:has(+ :is(input, select, textarea):required)::after {
    content: " *";
    color: #ef4444;
}

Styling Tables

/* Zebra striping */
tbody tr:nth-child(even) {
    background: rgba(255, 255, 255, 0.03);
}

/* Hover highlight on rows */
tbody tr:hover {
    background: rgba(59, 130, 246, 0.08);
}

/* Style the header row */
thead th {
    background: #1a1d27;
    font-weight: 600;
    text-align: left;
    padding: 0.75rem 1rem;
    border-bottom: 2px solid #3b82f6;
}

/* Right-align numeric columns */
td:nth-child(3),
td:nth-child(4) {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* Remove bottom border from last row */
tbody tr:last-child td {
    border-bottom: none;
}

Styling Navigation

/* Horizontal nav with active state */
.nav-links {
    display: flex;
    gap: 0.25rem;
}

.nav-links a {
    padding: 0.5rem 1rem;
    border-radius: 6px;
    color: #9ca3af;
    text-decoration: none;
    transition: all 0.2s;
}

.nav-links a:hover {
    background: rgba(255, 255, 255, 0.05);
    color: #e4e4e7;
}

.nav-links a[aria-current="page"],
.nav-links a.active {
    background: rgba(59, 130, 246, 0.15);
    color: #3b82f6;
    font-weight: 600;
}

/* Breadcrumb separator */
.breadcrumb a + span.separator::before {
    content: "/";
    margin: 0 0.5rem;
    color: #6b7280;
}

/* Mobile menu toggle */
.menu-toggle:checked ~ .nav-links {
    display: flex;
    flex-direction: column;
}

Responsive Card Layouts

/* Card with optional elements */
.card {
    background: #1a1d27;
    border-radius: 8px;
    overflow: hidden;
}

/* Cards with images get different padding */
.card:has(> img:first-child) {
    padding: 0;
}

.card:has(> img:first-child) > :not(img) {
    padding: 0 1.25rem;
}

.card:has(> img:first-child) > :last-child {
    padding-bottom: 1.25rem;
}

.card:not(:has(img)) {
    padding: 1.25rem;
}

/* Card actions pinned to bottom */
.card > .card-actions:last-child {
    margin-top: auto;
    padding-top: 1rem;
    border-top: 1px solid rgba(255, 255, 255, 0.06);
}

Dark Mode Toggle

/* Using a data attribute on <html> for theme switching */
[data-theme="dark"] {
    --bg: #0f1117;
    --text: #e4e4e7;
    --border: #2a2e3a;
}

[data-theme="light"] {
    --bg: #ffffff;
    --text: #1a1a2e;
    --border: #d1d5db;
}

/* Respect system preference as default */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #0f1117;
        --text: #e4e4e7;
        --border: #2a2e3a;
    }
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) {
        --bg: #ffffff;
        --text: #1a1a2e;
        --border: #d1d5db;
    }
}

Best Practices

Keep Specificity Low and Flat

The most maintainable CSS uses class selectors almost exclusively. Avoid IDs for styling. Avoid deeply nested descendant selectors. Aim for a specificity graph that stays flat across your stylesheet.

/* Bad: high specificity, hard to override */
#sidebar .widget-list li a.active { color: blue; }

/* Good: low specificity, easy to override */
.sidebar-link--active { color: blue; }

BEM Naming Convention

BEM (Block, Element, Modifier) is the most widely used CSS naming convention. It creates a flat, predictable naming structure that avoids nesting and specificity issues.

/* Block: standalone component */
.card { }

/* Element: part of a block (double underscore) */
.card__title { }
.card__body { }
.card__footer { }

/* Modifier: variation of a block or element (double dash) */
.card--featured { }
.card--compact { }
.card__title--large { }
<!-- HTML with BEM -->
<div class="card card--featured">
    <h3 class="card__title card__title--large">Featured Post</h3>
    <div class="card__body">
        <p>Card content goes here.</p>
    </div>
    <div class="card__footer">
        <a href="#" class="card__link">Read more</a>
    </div>
</div>

BEM's strength is that every selector is a single class. No nesting, no combinators, no specificity surprises. The naming convention itself communicates the relationship between elements, so you do not need the CSS cascade to establish context.

Utility-First with :where() for Overridability

/* Utility classes with :where() — zero specificity */
:where(.flex) { display: flex; }
:where(.grid) { display: grid; }
:where(.hidden) { display: none; }
:where(.text-center) { text-align: center; }
:where(.mt-4) { margin-top: 1rem; }
:where(.p-4) { padding: 1rem; }

/* Component styles easily override these */
.card {
    padding: 1.5rem;    /* Overrides :where(.p-4) without any issues */
}

Avoid Over-Qualifying Selectors

/* Bad: unnecessary type qualifier */
div.card { }          /* The "div" adds specificity but no value */
input[type="text"] { }  /* Sometimes unavoidable, but be aware of the cost */

/* Good: just the class */
.card { }

/* Bad: overly specific */
header nav ul li a { }

/* Good: direct class */
.nav-link { }

Use Logical Grouping in Your Stylesheet

/* 1. Reset / normalize */
*, *::before, *::after { box-sizing: border-box; }

/* 2. Base / typography */
body { font-family: 'Inter', sans-serif; }
h1, h2, h3 { line-height: 1.3; }

/* 3. Layout */
.container { max-width: 1200px; margin: 0 auto; }
.sidebar { }
.main-content { }

/* 4. Components */
.card { }
.btn { }
.modal { }

/* 5. Utilities (lowest specificity or last in source) */
.text-center { text-align: center; }
.sr-only { /* screen reader only */ }

Prefer Modern Selectors Over JavaScript

/* Instead of JavaScript to toggle parent styles: */
.form-group:has(input:focus) {
    /* Pure CSS — no JavaScript needed */
}

/* Instead of JavaScript for conditional styling: */
.card:has(img) { /* different layout when image present */ }

/* Instead of JavaScript for form validation feedback: */
input:user-invalid + .error-message {
    display: block;
}

Learn More

📚 DevToolbox resources on CSS:
CSS Selectors Cheat Sheet — Quick-reference table of every selector type
CSS Grid: The Complete Guide — Master two-dimensional layout
CSS Flexbox: The Complete Guide — Master one-dimensional layout
CSS Variables: The Complete Guide — Custom properties and dynamic theming
CSS Animations: The Complete Guide — Transitions, keyframes, and animation performance
CSS Performance Optimization — Critical rendering path, selector cost, and rendering layers

Frequently Asked Questions

What is the difference between :nth-child and :nth-of-type?

:nth-child(n) counts all sibling elements regardless of type. :nth-of-type(n) only counts siblings of the same element type. For example, p:nth-child(2) selects a <p> only if it is the second child overall. p:nth-of-type(2) selects the second <p> among its siblings, even if there are other element types between them. Use :nth-of-type() when the parent contains mixed element types and you want to count only specific elements.

How does CSS specificity work?

Specificity is a three-part weight calculated from the selectors in a CSS rule: ID selectors contribute to the first part, class selectors and pseudo-classes to the second, and type selectors and pseudo-elements to the third. Comparison is done left to right — one ID always beats any number of classes, and one class always beats any number of type selectors. When specificity is equal, the rule that appears later in the source code wins. Inline styles beat all selector-based specificity, and !important overrides everything.

Can I select a parent element based on its children in CSS?

Yes, with the :has() pseudo-class. For example, div:has(> img) selects any <div> that has a direct child <img>. The :has() selector is supported in all modern browsers (Chrome 105+, Firefox 121+, Safari 15.4+, Edge 105+). It enables parent selection, adjacent sibling conditions, and conditional styling that previously required JavaScript.

What is the difference between :is() and :where() in CSS?

Both :is() and :where() accept a selector list and match any element that matches at least one selector in the list. The difference is specificity: :is() takes the specificity of its most specific argument, while :where() always has zero specificity. Use :is() in component styles where you want normal specificity behavior. Use :where() for resets, defaults, and library styles where you want your styles to be easily overridden by consumers.

What does the + combinator do in CSS?

The + symbol is the adjacent sibling combinator. It selects an element that immediately follows another element, where both share the same parent. For example, h2 + p selects a <p> that comes directly after an <h2>. If there is any other element between them, the selector does not match. The general sibling combinator (~) is similar but matches all following siblings, not just the immediately adjacent one.

Should I use ID selectors for CSS styling?

Most modern CSS best practices recommend avoiding ID selectors for styling and using class selectors instead. IDs have very high specificity (1,0,0), which makes them hard to override without resorting to other IDs or !important. This leads to specificity escalation. Reserve IDs for JavaScript hooks (document.getElementById), fragment identifiers (anchor links), and form label associations. For styling, use class selectors, which offer the right balance of specificity and reusability.

Related Resources

CSS Selectors Cheat Sheet
Quick reference for every CSS selector type
CSS Beautifier
Format and prettify CSS code
CSS Minifier
Minify and compress CSS for production
CSS Grid: The Complete Guide
Master two-dimensional CSS layout
CSS Flexbox: The Complete Guide
Master one-dimensional layout with Flexbox
CSS Variables: The Complete Guide
Custom properties and dynamic theming