CSS

CSS Selectors: Every Way to Pick an Element

Type, class, id, attribute, combinators and pseudo-classes: learn how CSS selectors pick elements, with a live lab where you type your own.

RazakTerra
Reading time
12 min read
Comments
Discussion
CSS Selectors: Every Way to Pick an Element
On this page
  1. A selector is a yes-or-no question
  2. The basic selectors
  3. Type selectors
  4. Class selectors
  5. ID selectors
  6. The universal selector
  7. Attribute selectors
  8. Combinators: selecting by family
  9. Pseudo-classes: selecting by state and position
  10. State
  11. Position
  12. Logic: :not(), :is() and :has()
  13. Pseudo-elements: styling part of an element
  14. Putting selectors together
  15. A few habits that pay off
  16. Try it yourself
  17. Wrapping up

In the last post, every rule we wrote started with a tag name: h1, p, body. That’s a fine way to begin, but it runs out fast. A real page has a dozen paragraphs, and you want one of them to look like a warning. It has a list where the finished items should be crossed out. It has links, and only the ones that leave your site should get a little arrow.

Tag names can’t say any of that. Selectors can. CSS has a whole vocabulary for pointing at elements: by name, by class, by attribute, by where they sit in the tree, by what the reader is doing with them right now. It looks like a lot when you see it all at once, but it’s built from a handful of small pieces that snap together.

In this post we’ll learn those pieces one at a time, and try every one of them in a live lab. The matching in the lab is done by your browser, the same way it would match your own stylesheet, so anything you type there behaves exactly as it would on a real site.

A selector is a yes-or-no question

Here’s the idea to keep in your head for the whole post: a selector is a yes-or-no question the browser asks about every single element on the page. “Is this an li?” “Does it have the class done?” “Is it directly inside a ul?” Every element that answers yes gets the rule.

Longer selectors aren’t a different kind of thing. They’re just more specific questions, built by combining small ones.

Here’s the lab. Type a selector in the box, or pick one of the presets underneath. Matching elements are outlined in the page and marked in the source code beside it.

The selector lab
0 matches
index.html
<header>
<h1 id="title">Weekend plans</h1>
<nav>
<a href="/">Home</a>
<a href="https://weather.example">Weather</a>
</nav>
</header>
<h2>Saturday</h2>
<p>Keep it slow.</p>
<ul>
<li class="done">Buy bread</li>
<li>Fix the bike</li>
<li class="done urgent">Call grandma</li>
<li>Read a book</li>
</ul>
<h2>Sunday</h2>
<p class="note">Rain is likely.</p>
<label>
<input type="checkbox" checked>
Pack an umbrella
</label>

.done A class selector: every element whose class list contains done, including the one that also has urgent.

Basic
Combinators
Pseudo-classes
More than one
Type a selector, or pick one below. Matching elements light up in the page and in the source.

Spend a minute just trying things. Type p, then h2, then li. Then try something that doesn’t exist, like table, and notice that zero matches is a perfectly fine answer. Then type something broken, like li[. That’s not a question the browser can even read, and we’ll see why that matters at the end of the post.

The basic selectors

These five cover most of the CSS you’ll ever write.

Type selectors

A bare tag name selects every element of that type. You already know this one:

CSS
li {
padding: 4px 0;
}

Type selectors are great for setting sensible defaults, like the font for every paragraph. They’re too broad for anything more specific, because every li on the page gets the rule, including the ones in your navigation and your footer.

Class selectors

A dot followed by a name selects every element whose class attribute contains that name:

index.html
HTML
<li class="done">Buy bread</li>
<li class="done urgent">Call grandma</li>
styles.css
CSS
.done {
color: gray;
text-decoration: line-through;
}

The class attribute is a list of names separated by spaces, so the second item has two classes, and .done selects it too. Try .done and then .urgent in the lab.

Classes are the workhorse of CSS. You choose the names, you can put the same class on as many elements as you like, and one element can have as many classes as it needs. When you’re not sure which selector to use, a class is almost always the right answer.

ID selectors

A hash followed by a name selects the element with that id:

CSS
#title {
font-size: 40px;
}

An id must be unique on the page, so an ID selector picks at most one element. That makes it useful for links that jump to a section (href="#title") and for JavaScript, which often needs to find one specific element. For styling, though, prefer classes. An ID rule can’t be reused, and as we’ll see in the next post, it’s so strong that it’s painful to override later.

The universal selector

A lone * selects every element there is. You’ll mostly see it in resets that apply one rule to everything, like * { box-sizing: border-box; }, which we’ll meet in the box model post.

Attribute selectors

Square brackets select elements by their attributes. On their own, they check that the attribute exists. With an operator, they check its value too:

Selector Selects elements whose…
[checked] checked attribute exists, whatever its value
[type="checkbox"] type is exactly checkbox
[href^="https"] href starts with https
[href$=".pdf"] href ends with .pdf
[href*="example"] href contains example anywhere
Attribute selectors.

These are perfect for styling elements by what they already say about themselves. Every external link on a page, every link to a PDF, every text input: no extra classes needed. Try [href^="https"] in the lab, and notice that only the link to the weather site lights up.

Combinators: selecting by family

So far each selector has asked about an element on its own. Combinators ask about an element’s relatives. They go between two selectors, and the element being selected is always the one on the right.

Combinator Example Selects
Space (descendant) nav a An a anywhere inside a nav, at any depth
> (child) ul > li An li whose parent is a ul
+ (next sibling) h2 + p A p that comes immediately after an h2
~ (later sibling) h2 ~ p Any p after an h2, with the same parent
The four combinators.

The descendant combinator is the one you’ll use most, and it’s easy to miss because it’s just a space. nav a and nav.a look almost identical, but they ask completely different questions: the first means “an a inside a nav”, the second “a nav with the class a”.

Try h2 + p and h2 ~ p in the lab. The first selects “Keep it slow.” and “Rain is likely.”, the paragraphs right after each heading. The second selects the same two here, but on a page with several paragraphs after each heading, it would select all of them.

Pseudo-classes: selecting by state and position

A pseudo-class starts with a single colon, and selects elements based on something that isn’t written as a tag or attribute: what the reader is doing, or where the element sits among its siblings.

State

These change as the reader interacts with the page:

styles.css
CSS
a:hover {
text-decoration: underline;
}

button:focus-visible {
outline: 3px solid royalblue;
}

input:checked + label {
font-weight: bold;
}
  • :hover while the pointer is over the element.
  • :focus-visible when the element has keyboard focus. Always give interactive elements a visible focus style; keyboard users depend on it.
  • :checked for a ticked checkbox or a selected radio button.
  • :disabled, :required and :invalid for form fields, which pair beautifully with the HTML form validation you’ve already learned.

Try :checked in the lab, then untick the checkbox in the page. The selection follows, because the browser asks the question again whenever the answer might have changed.

Position

Structural pseudo-classes look at an element’s place among its siblings. :first-child and :last-child do what they say. :nth-child() is the powerful one: it takes a formula and selects every child whose position fits it.

How nth-child counts
li:nth-child(2n+1) { … }
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
Working it out
n2n+1Selects
02 × 0 + 1 = 1item 1
12 × 1 + 1 = 3item 3
22 × 2 + 1 = 5item 5
32 × 3 + 1 = 7item 7
42 × 4 + 1 = 9item 9
52 × 5 + 1 = 11item 11
62 × 6 + 1 = 13nothing (past the end)
A2
B1
Change A and B, or pick a preset. n counts up from 0, and every result that lands on a real position is selected.

The formula is An+B. The browser plugs in n = 0, 1, 2, 3… and selects the children at each position it gets back. A few things worth noticing in the table:

  • 2n+1 gives 1, 3, 5, 7: the odd children. It’s so common that odd is a keyword for it, and even means 2n.
  • With A set to 0, there’s no n left, so 3 simply means “the third child”.
  • n+4 means “the 4th and everything after it”.
  • -n+3 counts down: 3, 2, 1, then 0 and below, which aren’t real positions. So it means “the first three”. That one looks like a puzzle the first time, and the table is the fastest way to see through it.

Positions start at 1, not 0, and count every element child of the parent, whatever its type. li:nth-child(2) means “an li that is also the second child”, not “the second li”. In a list, those are the same thing.

Logic: :not(), :is() and :has()

Three pseudo-classes take other selectors inside their parentheses:

  • :not(.done) selects elements that don’t match. li:not(.done) is every unfinished item.
  • :is(h1, a) matches anything in the list, which keeps long selectors short: header :is(h1, a) instead of header h1, header a.
  • :has(.urgent) selects an element that contains something matching. ul:has(.urgent) styles the list itself, because one of its items is urgent. For years CSS had no way to select a parent based on its children. :has() finally added one, and it works in every modern browser.

Pseudo-elements: styling part of an element

A pseudo-element has two colons, and selects a part of an element that isn’t an element on its own:

styles.css
CSS
p::first-line {
font-weight: bold;
}

input::placeholder {
color: gray;
}

.note::before {
content: "Note: ";
font-weight: bold;
}

::first-line and ::placeholder style a piece of text you can’t wrap in a tag yourself. ::before and ::after go a step further and create a little piece of content at the start or end of the element, which is how many icons and decorations are made. They need a content property, even if it’s just content: "".

The colons are the easy way to tell them apart: one colon for a state (pseudo-class), two for a part (pseudo-element). You’ll sometimes see :before with one colon in older code. Browsers still accept that for the four oldest pseudo-elements, but write two.

Putting selectors together

Everything above can be combined, and there are just two rules for doing it.

No space means “and”. .done.urgent selects elements with both classes. li.done is an li that has the class done. a[href^="https"]:hover is an external link the pointer is over.

A comma means “or”. h1, h2 is a selector list: the rule applies to anything matching either one. It saves you from writing the same declarations twice.

A few habits that pay off

  • Keep selectors short. .nav-link is easier to read, faster to match and easier to override than body header nav ul li a.
  • Style with classes, not IDs. Save IDs for links and JavaScript.
  • Don’t qualify classes with tags you don’t need. .warning works on a p, a div or a li. p.warning only works on the first, and breaks the day someone uses the class on something else.

Try it yourself

Here’s the to-do list from the lab, with a stylesheet that uses many of the selectors from this post. Change them, add your own, and see what each one picks.

Selector practice
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Weekend plans</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1 id="title">Weekend plans</h1>
  <nav>
    <a href="/">Home</a>
    <a href="https://weather.example">Weather</a>
  </nav>
  <ul>
    <li class="done">Buy bread</li>
    <li>Fix the bike</li>
    <li class="done urgent">Call grandma</li>
    <li>Read a book</li>
  </ul>
  <p class="note">Rain is likely.</p>
</body>
</html>
Result
Cmd/Ctrl + Enter runs it · Esc leaves the editor

Wrapping up

Every selector, however long, is the same thing underneath: a yes-or-no question asked of every element on the page. The basic selectors ask about the element itself, combinators ask about its family, pseudo-classes ask about its state and position, and you can join any of them with “and” (no space) or “or” (a comma).

Here’s the short version to keep:

  1. Type (li), class (.done), ID (#title), universal (*) and attribute ([type="checkbox"]) selectors ask about one element.
  2. Reach for a class first. Save IDs for links and JavaScript.
  3. A space means “inside”, > “directly inside”, + “right after” and ~ “somewhere after”. The element on the right is the one styled.
  4. One colon is a state or position (:hover, :nth-child(odd)). Two colons are a part (::before).
  5. :nth-child(An+B) plugs in n = 0, 1, 2… and selects every real position it gets.
  6. No space joins selectors with “and”. A comma lists them with “or”.
  7. One invalid selector in a list drops the whole rule.

Next up: what happens when several of these selectors pick the same element and disagree. That’s the cascade, and specificity, and it explains most of the “why isn’t my CSS working?” moments you’ll ever have. See you there! 🎯

Written by

Razak

Writes about the web, slowly, and builds small tools on the side.

Discussion

Comments

Loading comments…

Keep reading