Selectors that pay rent
The five or six selectors that earn their place in almost every stylesheet.
CSS has dozens of selectors. Most stylesheets, even on big sites, lean on six or seven of them and barely touch the rest. Memorizing the long list early is wasted effort — the goal here is to recognize the handful that pay their rent every day, and to feel comfortable reaching for them.
A selector is the part of a CSS rule that decides which elements get the styles. The browser reads a selector and asks "does this element fit this description?" Get the description right and the rule applies; get it wrong and nothing happens, silently.
Type, class, and id
The three workhorse selectors are the type selector, the class selector, and the id selector. Almost every stylesheet you ever read or write will be built mostly out of these three.
A type selector is just an HTML tag name. p matches every paragraph; h1 matches every top-level heading; a matches every link. No punctuation needed — write the tag and you have a selector for it.
A class selector starts with a dot, like .warning. It matches every element that has that class on its class="…" attribute. One element can have many classes (class="warning small" would match .warning and .small separately), and one class can sit on many elements. Classes are how you give a name to a role — "this is a warning", "this is a card" — without tying that name to a particular HTML tag.
An id selector starts with a hash, like #main. It matches the one element with id="main". Ids are supposed to be unique on the page — the browser will not stop you from putting the same id on two elements, but everything that depends on ids (links to #main, JavaScript that calls getElementById, your own sanity) breaks if you do.
p { color: #2d2418; }
.warning { background: #fff3e0; }
#main { max-width: 720px; }Three rules, three selectors, three jobs. The first targets every paragraph by tag. The second targets the role "warning" wherever it appears, which might be a <p>, a <div>, or a <section>. The third targets the single element with id="main".
A practical rule of thumb: reach for a class first. Type selectors are useful for setting baseline defaults (p, h1, a). Ids are useful for things that genuinely are one-of-a-kind on the page (the main content area, the page header). Everything else is a class.
A class can name what something is (.button) or what role it plays right now (.is-active). Both are fine — just don't bake visual details into the name. .green-button ages badly the first time the brand colour changes.
<button class="primary"> on the page to have a navy background, but plain <button> elements should stay grey. Which selector is the cleanest fit?Attribute selectors
An attribute selector matches by the value of an HTML attribute, written in square brackets. The most common case is on inputs and links, where the type or destination matters more than the tag.
input[type="email"] { border-color: #c96442; }
a[href^="https://"] { color: #2a5d8f; }
img[alt=""] { outline: 2px solid red; }The first rule targets only <input> elements whose type attribute is exactly "email". The second uses ^=, which means "starts with" — it catches every link whose href starts with https://, useful for marking external links. The third matches images whose alt attribute is empty, a quick way to flag accessibility problems while you build.
A few small operators are worth recognizing: = is exact match, ^= is "starts with", $= is "ends with", and *= is "contains anywhere". You will not reach for them daily, but when you need them they are exactly what you need.
Combining selectors
Selectors really start to earn their rent when you combine them. The two combinators that come up constantly are the descendant combinator (a space) and the child combinator (a >).
A space between two selectors means "the second one, anywhere inside the first." nav a matches every link inside any <nav>, no matter how deeply nested.
A > between two selectors means "the second one, but only as a direct child of the first." nav > a matches links that sit immediately inside the <nav>, but not links wrapped inside a <ul> or a <div> inside the nav.
<nav> <a href="/">Home</a> <!-- matches both nav a and nav > a --> <ul> <li><a href="/blog">Blog</a></li> <!-- matches nav a but NOT nav > a --> </ul> </nav>
The first link is a direct child of the nav, so both selectors match. The second link is wrapped in <ul><li> first, so only the descendant version (nav a) reaches it. The child combinator (>) stops at one level. Most of the time the descendant version is what you want; reach for > when you specifically need to not style the deeper ones.
<header><h1>Hi</h1><div><h1>Sub</h1></div></header>, which selector matches only the first heading?Pseudo-classes you'll actually use
A pseudo-class is a colon-prefixed keyword tacked onto a selector, like a:hover. It matches elements only when they are in a certain state. There are dozens of them; four of them carry their weight on almost every page.
:hover matches an element while the user's pointer is over it. The classic use is colour-changing links and buttons, but be aware: pointer devices are not the only kind of device. Touch screens have no real "hover" state, so never put information only in a hover style.
:focus matches an element that has keyboard focus — usually because the user tabbed to it. This is the one you cannot skip. Removing the focus ring with outline: none and not replacing it locks keyboard users out of your page. If the default ring clashes with your design, replace it with something visible, do not remove it.
:focus-visible is the gentler version of :focus: it only triggers when the focus came from a keyboard, not a mouse click. Most pages now use :focus-visible for the visible ring and let :focus handle the underlying state.
:not(...) flips a selector: button:not(.primary) matches every button that does not have the class primary. It pairs well with type and class selectors when you want to express "everything except this one variant" without adding a class to all the others.
a:hover { text-decoration: underline; }
button:focus-visible {
outline: 2px solid #c96442;
outline-offset: 2px;
}
button:not(.primary) {
background: #eee;
}The hover rule underlines links when the pointer is over them. The focus-visible rule gives keyboard-focused buttons a clear orange ring two pixels out from the edge — visible enough to never be missed. The third rule paints every button grey unless it has the primary class, so styling the primary case becomes a separate, simple rule.
outline: none on a focusable element with no replacement is one of the most common accessibility regressions on the web. If you remove the default focus ring, you must give it a visible replacement.
button:hover { outline: 2px solid orange; } as your only "selected" style. A keyboard-only user tabs to the button. What do they see?Selector lists
When two selectors should share the same declarations, you can list them with a comma between. A comma means "either of these" — the rule applies to anything matching any of the listed selectors.
h1, h2, h3 {
font-family: var(--font-display);
line-height: 1.2;
}That single rule applies to every <h1>, every <h2>, and every <h3>. It is exactly equivalent to writing three identical rules — just less typing and easier to keep in sync.
The comma trips up beginners because it looks like a combinator (the space and > from earlier), but it is the opposite. A space narrows — header h1 is headings inside the header. A comma widens — header, h1 is every header element OR every h1, including h1s outside the header. If a rule suddenly starts applying to elements you did not expect, check whether you typed a comma you meant to be a space.
nav a, .button { padding: 8px; } match?<input type="email" required>. Which selector targets only required email inputs?