A selector is how you say which elements a rule applies to. There are a lot of them, and the useful thing is not to memorise all of them but to know which handful covers almost every job — and to know how to combine them when it does not.
What are the selectors you will use every day?
Five cover the vast majority of stylesheets: the type selector, the class selector, the id selector, the attribute selector, and the combinators that join them.
/* type: every h2 on the page */
h2 { font-size: 1.5rem; }
/* class: any element with that class */
.card { border: 1px solid #ddd; }
/* id: one specific element */
#site-header { position: sticky; top: 0; }
/* attribute: elements with that attribute, whatever the value */
input[required] { border-color: #b3261e; }
/* combinator: a descendant of something */
.card h3 { margin-top: 0; }
The class selector is the workhorse. It is reusable, it carries meaning, and it keeps the presentation out of the markup — the same class can appear on any number of elements of any type. The id selector is for one element that is genuinely unique, and using it for styling is usually a sign that a class would serve better.
What do the combinators do?
Four combinators connect two selectors, and the difference between them is what sits between the two parts.
/* descendant: any depth */
nav a { color: inherit; }
/* child: exactly one level down */
nav > a { padding: 0.5rem; }
/* adjacent sibling: the element right after */
h2 + p { margin-top: 0; }
/* general sibling: any element after */
h2 ~ p { color: #555; }
The child combinator is the one that earns its place, because it stops styles leaking down into deeper structures. nav a styles every link inside the navigation, however deeply nested, while nav > a styles only the links that are direct children — which is usually what you meant.
The sibling combinators are useful for spacing. h2 + p removes the top margin from the paragraph that follows a heading, which is the standard way to stop the gap between a heading and its first paragraph looking too large.
Which pseudo-classes matter most?
A pseudo-class selects elements by their state rather than by their identity, and four of them do most of the work in an interface.
a:hover { color: #800d4d; } /* the pointer is over it */
input:focus-visible { outline: 2px solid #800d4d; } /* keyboard focus */
li:nth-child(2n) { background: #f7f7f7; } /* every second one */
input:disabled { opacity: 0.5; cursor: not-allowed; }
:nth-child is more capable than it looks. The argument is a formula, and the keyword forms cover most needs: :nth-child(odd) and :nth-child(even) for alternating rows, :nth-child(1) for the first, :nth-last-child(1) for the last, and :nth-child(2n+1) for every other one starting at the first.
/* the first three items in a row of five */
.grid > :nth-child(-n+3) { grid-column: span 3; }
What can attribute selectors match?
More than equality. The operators cover presence, prefixes, suffixes, substrings and values from a list, and the last one is the one that pays off in real forms.
[type="email"] { ... } /* exact value */
[type^="tel"] { ... } /* value starts with "tel" */
[href$=".pdf"] { ... } /* value ends with ".pdf" */
[class*="button"] { ... } /* value contains "button" */
[lang|="en"] { ... } /* "en" or "en-GB" */
Because a page can carry several classes, [class*="button"] matches an element whose class list contains the word — which makes it a way to style variants of a component without adding another attribute. And [href^="mailto:"] is a neat way to style email links differently from web links, with no extra class in the markup.
How do you choose a selector?
Start from the most general that still works, and go more specific only when you must. Styling by tag name is fine for element defaults like headings and lists. Styling by class is the right answer for components. Styling by id is for one unique element, and styling by structure — a selector like body > div > ul > li > a — is a maintenance trap, because it breaks the moment the markup changes.
/* brittle: tied to the exact nesting of the markup */
body > div > ul > li > a { color: blue; }
/* robust: tied to a class that means something */
.nav-link { color: blue; }
Two rules of thumb keep a stylesheet maintainable. Never style by position in the document, and prefer a class that describes what the thing is over one that describes where it sits. .nav-link survives a redesign; .second-menu-item does not.
Which selectors are worth avoiding?
Not all selectors are equal. A few of them work perfectly and cost you nothing; others work and then make the next change to the page a fight.
The one to avoid is the long descendant chain. .page .content .sidebar .card .card__title matches exactly one element on your page and will break the moment any part of that structure changes. The specificity of that selector is also enormous, so it will beat almost everything you write later, which means the only way to override it is an even longer chain or !important.
The second is the tag-plus-class-plus-attribute combination. div.card[data-state="open"] is three type-level steps that add nothing the class does not already say. Write .card[data-state="open"] and it reads the same and behaves the same.
The third is styling by position. div:nth-of-type(3) ties a rule to the order of the markup, so inserting a new element above it silently restyles the wrong thing. Reach for a class when the rule is about a particular item.
<!-- fragile: the style depends on the order of the markup -->
<ul class="menu">
<li>Home</li>
<li class="is-current">Docs</li>
<li>Support</li>
</ul>/* stable: the class says what it is */
.menu .is-current {
color: #1f5fa9;
font-weight: 600;
}The pattern to aim for is a short, meaningful class on each element you want to style, and a selector that is at most a class or two deep. That keeps specificity low enough that you can still override things later, and it keeps the stylesheet readable to whoever touches the page next - including you, in six months.
Quick reference
- Type, class, id, attribute and combinators cover almost every real stylesheet.
- Use
>to stop styles bleeding into deeper nesting. h2 + premoves the gap between a heading and the paragraph after it.:nth-childtakes a formula, and the keyword forms cover most cases.- Attribute selectors can match prefixes, suffixes and substrings, which is useful for component variants.
- Style by class, not by position in the document.
Try it yourself: write a selector that targets every third item in a list, one that targets every link whose address ends in a particular file type, and one that targets only the first child of a container. Use the developer tools to confirm each one matches what you expected before you write the rule.