CSS selectors are the patterns you write to target HTML elements and apply styles to them. Knowing your selectors well means writing less CSS, avoiding unnecessary classes, and targeting elements with surgical precision. This guide covers every selector type — from the basics to combinators, attribute selectors, pseudo-classes, and pseudo-elements — with practical examples for each.

The basic selectors

Type Selector

Targets every element of a given HTML tag. The broadest selector — use it for base styles and resets.

/* Targets every paragraph */
p {
  line-height: 1.6;
}

/* Targets every heading */
h2 {
  font-size: 1.5rem;
}

Class Selector

Targets elements with a specific class attribute. The most commonly used selector in day-to-day CSS — reusable across multiple elements.

.card {
  border-radius: 8px;
  padding: 1.5rem;
}

/* Multiple classes on one element */
.btn.btn-primary {
  background: #3b82f6;
}

ID Selector

Targets a single element with a specific ID. IDs must be unique per page. The ID selector has very high specificity — it overrides class and type selectors. Avoid using ID selectors for styling in most cases; reserve IDs for JavaScript hooks and anchor links.

#main-nav {
  position: sticky;
  top: 0;
}

Universal Selector

Targets every element on the page. Most commonly used in CSS resets and for applying box-sizing globally.

/* Apply box-sizing to every element */
*,
*::before,
*::after {
  box-sizing: border-box;
}

Combinators — Selecting by Relationship

Combinators define the relationship between two selectors. They let you target elements based on where they sit in the HTML structure.

Descendant Combinator (space)

Targets an element that is anywhere inside another element — not necessarily a direct child.

/* Any <a> inside .nav, at any depth */
.nav a {
  text-decoration: none;
  color: inherit;
}

Child Combinator (>)

Targets an element that is a direct child of another — one level deep only.

/* Only direct <li> children of .menu, not nested ones */
.menu > li {
  display: inline-block;
}

Adjacent Sibling Combinator (+)

Targets an element that immediately follows a specified element, sharing the same parent.

/* <p> that comes immediately after an <h2> */
h2 + p {
  margin-top: 0.5rem;
  font-size: 1.1rem;
}

/* Remove top margin from label immediately after a checkbox */
input[type="checkbox"] + label {
  margin-top: 0;
}

General Sibling Combinator (~)

Targets all matching elements that follow a specified element, sharing the same parent — not just the immediately adjacent one.

/* All <p> elements that follow an <h2>, not just the first */
h2 ~ p {
  color: #475569;
}

Attribute Selectors

Attribute selectors target elements based on the presence or value of their HTML attributes. They are extremely powerful for styling form inputs, links, and custom data attributes.

/* Has the attribute (any value) */
[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

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

/* Value starts with */
[href^="https"] {
  color: green; /* secure links */
}

/* Value ends with */
[href$=".pdf"] {
  /* style PDF links differently */
  padding-right: 1.25rem;
  background: url("/icons/pdf.svg") no-repeat right center;
}

/* Value contains */
[class*="btn"] {
  cursor: pointer;
  border-radius: 4px;
}

/* Value is a space-separated word */
[class~="active"] {
  font-weight: bold;
}

/* Value equals or starts with (useful for language codes) */
[lang|="en"] {
  font-family: Georgia, serif;
}

Attribute selectors are case-insensitive by default for HTML attributes. Add i before the closing bracket for explicit case-insensitivity, or s for case-sensitive matching.

/* Case-insensitive match */
[type="text" i] { }

/* Case-sensitive match */
[data-status="Active" s] { }

Pseudo-Classes — Selecting by State

Pseudo-classes target elements based on their current state, position, or user interaction — information that is not visible in the HTML source.

User Interaction States

/* Mouse hover */
.btn:hover {
  background: #1d4ed8;
}

/* Keyboard focus — never remove, essential for accessibility */
.btn:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

/* Focus only via keyboard, not mouse */
.btn:focus-visible {
  outline: 2px solid #3b82f6;
}

/* Active (being clicked) */
.btn:active {
  transform: scale(0.97);
}

/* Visited links */
a:visited {
  color: #7c3aed;
}

Form States

/* Valid and invalid input */
input:valid   { border-color: #22c55e; }
input:invalid { border-color: #ef4444; }

/* Checked checkbox or radio */
input:checked + label {
  font-weight: bold;
}

/* Disabled input */
input:disabled {
  background: #f1f5f9;
  cursor: not-allowed;
}

/* Required field */
input:required {
  border-left: 3px solid #f59e0b;
}

/* Placeholder shown (input is empty) */
input:placeholder-shown {
  border-color: #cbd5e1;
}

Structural Pseudo-Classes

These target elements based on their position within their parent. They are the foundation of pattern-based styling without adding extra classes.

/* First and last child */
li:first-child { border-top: none; }
li:last-child  { border-bottom: none; }

/* First and last of a specific type */
p:first-of-type { font-size: 1.1rem; }
p:last-of-type  { margin-bottom: 0; }

/* Every other item (zebra striping) */
tr:nth-child(even) {
  background: #f8fafc;
}

/* Every third item starting from the first */
li:nth-child(3n+1) {
  color: #3b82f6;
}

/* Only child */
p:only-child {
  font-style: italic;
}

/* Empty element */
div:empty {
  display: none;
}

Other Useful Pseudo-Classes

/* Negation — everything except .active */
li:not(.active) {
  opacity: 0.6;
}

/* Multiple negations (modern CSS) */
li:not(.active):not(.disabled) {
  cursor: pointer;
}

/* :is() — groups selectors with shared styles */
:is(h1, h2, h3, h4) {
  font-family: Georgia, serif;
  line-height: 1.2;
}

/* :where() — like :is() but zero specificity */
:where(h1, h2, h3) {
  margin-top: 1.5rem;
}

/* :has() — parent selector (select parent based on child) */
.card:has(img) {
  padding-top: 0; /* card containing an image gets no top padding */
}

label:has(+ input:required)::after {
  content: " *";
  color: #ef4444;
}

Pseudo-Elements — Selecting Virtual Parts

Pseudo-elements target a specific part of an element, or insert generated content. They use double-colon syntax (::) to distinguish them from pseudo-classes.

/* First line of a paragraph */
p::first-line {
  font-variant: small-caps;
}

/* First letter of a paragraph (drop cap) */
p::first-letter {
  font-size: 3em;
  float: left;
  line-height: 1;
  margin-right: 0.1em;
}

/* Insert content before and after */
.quote::before { content: "\201C"; }
.quote::after  { content: "\201D"; }

/* Style selected text */
::selection {
  background: #bfdbfe;
  color: #1e3a5f;
}

/* Style placeholder text */
input::placeholder {
  color: #94a3b8;
  font-style: italic;
}

Selector Specificity

When two selectors target the same element, the browser uses specificity to decide which rule wins. Specificity is calculated as a three-part score: (ID, Class/Attribute/Pseudo-class, Type/Pseudo-element).

/* Specificity: (0, 0, 1) — type selector */
p { color: black; }

/* Specificity: (0, 1, 0) — class selector */
.text { color: blue; }

/* Specificity: (0, 1, 1) — class + type */
p.text { color: green; }

/* Specificity: (1, 0, 0) — ID selector */
#intro { color: red; }

/* Inline style — always wins (specificity: 1-0-0-0) */
<p style="color: purple">

Key rules to remember:

  • ID selectors always beat class selectors, regardless of how many classes are chained
  • !important overrides everything — avoid using it except in utility classes or resets
  • :is(), :not(), and :has() take the specificity of their most specific argument
  • :where() always has zero specificity — useful for base styles you want to be easily overridden
  • When specificity is equal, the rule that appears last in the stylesheet wins

Selector Performance Tips

Browsers read selectors from right to left — the rightmost part is matched first, then the browser walks up the DOM to verify the rest. This means the rightmost selector (the key selector) should be as specific as possible to limit the number of elements the browser needs to check.

  • Avoid overly broad key selectors: * { } and div { } force the browser to check every element.
  • Avoid deep descendant chains: .nav ul li a span makes the browser walk five levels up the DOM for every <span> on the page.
  • Prefer class selectors: They are fast, reusable, and have predictable specificity.
  • Avoid * as a key selector in combinations: .parent * checks every element in the document to see if it is inside .parent.

In practice, selector performance is rarely the bottleneck on modern browsers — layout, paint, and JavaScript execution are far more impactful. But clean, focused selectors make your CSS easier to maintain.

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) counts only siblings of the same element type. If your list contains mixed element types, :nth-of-type gives more predictable results. For a consistent list of the same element, both work the same.

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

Both group selectors to share a set of styles. The difference is specificity. :is() takes the specificity of its most specific argument — so :is(#id, .class) has ID-level specificity. :where() always contributes zero specificity, making it ideal for base and reset styles that should be easy to override.

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

Yes — using the :has() pseudo-class, which is now supported in all modern browsers. .card:has(img) selects any .card that contains an <img>. This was impossible in CSS for years and required JavaScript. :has() is one of the most powerful additions to modern CSS.

What is the difference between the child combinator and the descendant combinator?

The descendant combinator (a space) targets an element anywhere inside another — at any depth. The child combinator (>) targets only direct children, one level deep. Use > when you want to avoid unintentionally styling deeply nested elements of the same type.

How do I select every element except one?

Use the :not() pseudo-class. For example, li:not(.active) selects all list items except those with the active class. Modern CSS also allows multiple arguments: li:not(.active, .disabled) excludes both classes in one rule.

Why should I avoid ID selectors for styling?

ID selectors have very high specificity — higher than any number of chained class selectors. This makes them hard to override cleanly and leads to specificity wars where you end up using !important or doubling selectors just to win. IDs are also unique per page, so they are not reusable. Class selectors give you the same targeting ability with manageable, predictable specificity.