PLAINFINCH learn by building

Selectors

You will be able to pick exactly the elements you want to style, using classes and other selectors.

How does a selector pick elements?

A selector is a pattern. The browser tests it against every element in the document and applies the rule to each element that fits. Patterns can describe an element's type, its classes, its id, its attributes, its position relative to other elements, or its current state such as being hovered.

Good selectors say what you mean and nothing more. .price reads as "anything marked as a price". div > div > span:nth-child(2) reads as a puzzle, and it breaks the moment someone wraps the price in another element. Aim for selectors a teammate understands without opening the HTML.

FigureA small DOM tree of an article with an h2, two paragraphs and a div holding a third paragraph, and a table showing which paragraphs four combinator selectors match.A small DOM tree of an article with an h2, two paragraphs and a div holding a third paragraph, and a table showing which paragraphs four combinator selectors match.
The space combinator reaches any descendant, > only direct children, + only the very next sibling, and ~ every later sibling.

When should you use type, class and id selectors?

A type selector such as p, h2 or button matches every element of that name. Use type selectors for site-wide defaults: body text, headings, links, tables. They are the lowest-priority selectors, so classes override them easily.

A class selector starts with a dot and matches any element whose class attribute contains that word. An element can carry several classes, separated by spaces, and a class can appear on any number of elements. Most of your styling should hang on classes because they are reusable and easy to override.

An id selector starts with # and matches the one element with that id. Ids are for anchors (href="#walks"), form labels and JavaScript hooks. Avoid them in stylesheets. An id selector is so specific that overriding it later means writing another id selector or reaching for !important.

Class and id names are case-sensitive. .Rare does not match class="rare". Stick to lowercase words joined by hyphens and the problem never comes up.

h2 {
  font-family: Georgia, serif;
}

.rare {
  color: #a3341f;
}

.badge.rare {
  border: 2px solid currentColor;
}

What do the universal and attribute selectors do?

The universal selector * matches every element. Its common job is a box-sizing reset: *, *::before, *::after { box-sizing: border-box; }. It carries no specificity, so anything else overrides it.

Attribute selectors match on HTML attributes. [disabled] matches any element with that attribute present. [type="email"] needs an exact value. [href^="https"] matches values that start with a string, [href$=".pdf"] values that end with one, and [class*="card"] values that contain it anywhere.

They are handy when the HTML already says what you need: marking links to PDFs, styling every text-like input, or showing which nav link has aria-current="page". Styling from aria-current also keeps the visual state and the accessible state in sync, because there is only one source of truth.

a[href$=".pdf"]::after {
  content: " (PDF)";
}

.club-nav [aria-current="page"] {
  font-weight: 700;
}

How do you give several selectors the same styles?

Separate them with commas. h1, h2, h3 { font-family: Georgia, serif; } is one rule with a selector list, and it saves you from repeating the same declarations three times. Put each selector on its own line when the list gets long.

One catch: if any selector in the list is invalid, the browser drops the whole rule. A stray trailing comma, a typo in a pseudo-class, or a selector the browser doesn't support yet all have this effect. The forgiving :is() pseudo-class avoids it, because invalid entries inside :is() are skipped individually: :is(h1, h2, h3) behaves like the list but survives a bad entry.

h1,
h2,
h3 {
  font-family: Georgia, serif;
  line-height: 1.2;
}

How do combinators target elements by their position?

Combinators join two selectors and describe a relationship. A space is the descendant combinator: .sightings li matches any list item anywhere inside .sightings, however deeply nested.

The > child combinator only matches direct children. .sightings > li skips list items in a nested list. Use it when your component can contain other components and you don't want styles leaking inward.

The + next-sibling combinator matches an element that comes immediately after another with the same parent. h2 + p styles the first paragraph after each heading, perfect for an intro paragraph or removing a top margin. The ~ subsequent-sibling combinator matches every later sibling with the same parent, however far down.

Combinators only look down the tree or forward through siblings. To style a parent based on its children, CSS now has :has(), supported in all current browsers since late 2023: .card:has(img) matches cards that contain an image.

.sightings > li {
  padding-block: 4px;
}

h2 + p {
  margin-top: 0;
  font-size: 1.125rem;
}

What is a pseudo-class?

A pseudo-class starts with a single colon and matches an element in a particular state or position. a:hover matches a link under the pointer. input:focus-visible matches a field focused by keyboard. li:first-child and li:nth-child(odd) match by position. :checked, :disabled and :invalid read form state.

Pseudo-classes let you react to the user and to structure without adding classes by hand. A later lesson covers them in detail. For now, recognise the pattern: a normal selector, a colon, a keyword. Pseudo-elements such as ::before use two colons and create a part of an element rather than selecting a state.

.club-nav a:hover {
  text-decoration: underline;
}

.sightings li:nth-child(odd) {
  background: #f3f1ea;
}

How do you keep selectors readable as a site grows?

Prefer one class per job. .sighting-rare or .rare tells you what the element is. A chain like main section ul li span tells you only where it happened to sit last Tuesday, and any change to the HTML breaks it.

Keep selectors to two or three parts. Deep chains raise specificity, so the next person has to write an even longer selector to override yours, and the stylesheet slowly turns into an arms race.

Avoid styling with ids and avoid qualifying classes with a tag (ul.sightings) unless you need to. The tag adds specificity and stops the class working on other elements.

When an AI tool writes your CSS, watch for very long descendant chains copied from the DOM, div repeated several times, and classes that describe appearance such as .red-text. Rename those to say what the content is, so the stylesheet survives a redesign.

How can you test a selector quickly?

Open the DevTools console and run document.querySelectorAll(".sightings > li"). It returns every element the selector matches, using the same rules the browser uses for CSS. If the list is empty, your selector is wrong before any styling question even comes up.

In the Elements panel, Ctrl+F (Cmd+F on a Mac) also accepts a CSS selector and highlights each match in turn. That is a fast way to check how many elements a rule will hit.

Field notes

  • A selector is a pattern the browser checks against every element.
  • Type selectors suit site-wide defaults, and classes suit specific jobs.
  • Commas let several selectors share one set of styles.
  • Short class names that describe the job stay readable as a site grows.

Easy to get wrong

The HTML uses class="rare", so the selector needs a dot. A hash only matches an id.

A trailing comma makes the selector list invalid, and the browser discards the whole rule.

A space means descendant. When the class sits on the link itself, write it with no space after the tag.

Questions people ask

Should I ever use ids in CSS?

Rarely. Their high specificity makes later overrides awkward. Use a class, and keep ids for anchors, labels and scripts.

Is a long selector slower?

Not in any way you will notice. Modern engines match millions of selectors per second. Keep selectors short for readability and easier overrides.

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

Both take a list of selectors and forgive invalid entries. :is() takes the specificity of its most specific argument, while :where() always has zero specificity.

Can a selector match text inside an element?

No. Selectors match elements, attributes and states. To style part of a sentence, wrap it in an element such as <strong> or <span class="...">.

Keep going