Module 2: Selectors & Combinators
Basic Selectors: Universal, Type, Class, ID and Attribute
How each basic selector matches elements, how the browser actually evaluates them, and when to use a class instead of an ID.
A selector is a question you ask the document: "which elements do I mean?" Every CSS rule starts with one, so getting this right decides how maintainable your stylesheet is a year from now.
The five basic selectors#
* { box-sizing: border-box; } /* universal: every element */
p { line-height: 1.6; } /* type: every <p> */
.warning { color: #b42318; } /* class: class="warning" */
#site-header { position: sticky; } /* ID: id="site-header" */
[type="email"] { width: 100%; } /* attribute: type="email" */
Type selectors match by tag name. They are great for baseline styles on elements that always look the same (body, p, a), and a poor choice for components, because every paragraph on the page gets hit.
Class selectors match the class attribute. An element can carry many classes, and many elements can share one. This is the workhorse of CSS.
ID selectors match one element by id. The id must be unique in the document. IDs are valuable as link targets and for JavaScript or labels, but you should rarely style with them, for reasons you will see in the specificity lesson. One ID selector outweighs any number of class selectors.
The universal selector * matches everything. It is fine in a reset. The old claim that it is slow does not hold up in modern browsers, but it has zero specificity and no meaning, so use it deliberately.
Combining selectors on one element#
Writing selectors with no space between them means the element must match all of them.
.btn.primary { background: #0b5fff; } /* has both classes */
a.btn { text-decoration: none; } /* an <a> that has class btn */
input[type="checkbox"].large { } /* all three conditions */
Compare that with a space:
.btn .primary { } /* an element with class primary INSIDE an element with class btn */
One space turns "and" into "inside". That single character causes more confusion than anything else in selector writing. When a rule matches nothing, check for a stray or missing space.
Grouping#
A comma lets several selectors share one block:
h1, h2, h3 {
font-family: Georgia, serif;
line-height: 1.2;
}
Remember from Module 1 that one invalid selector in the list kills the whole rule.
Attribute selectors in detail#
Attribute selectors read HTML attributes and let you style based on data, without adding classes.
[disabled] { opacity: 0.5; } /* attribute is present */
[type="submit"] { font-weight: 700; } /* exact match */
[class~="card"] { } /* whitespace-separated word */
[lang|="en"] { } /* "en" or "en-" prefix: en, en-US */
a[href^="https://"] { } /* starts with */
a[href$=".pdf"] { } /* ends with */
a[href*="example.com"] { } /* contains */
a[href$=".PDF" i] { } /* i flag: case-insensitive */
A practical example is marking external links and file downloads:
a[href^="http"]:not([href*="mysite.com"])::after {
content: " ↗";
}
a[href$=".pdf"]::before {
content: "PDF ";
font-size: 0.75em;
font-weight: 700;
}
Attribute values are case-sensitive by default (except for a few HTML attributes like type). Add the i flag when that matters.
How the browser reads a selector#
Humans read nav ul li a left to right. Browsers match it right to left. For each element, the engine asks "is this an a?" first, and only if yes does it check whether there is an li ancestor, then a ul, then a nav.
Selector: nav ul li a
Matching: ◄─────────────── start with the key (rightmost) selector,
then walk UP the tree to verify the rest
The rightmost part is called the key selector. It is why .menu a has to look at every a on the page, while .menu-link looks only at elements with that class. Browsers are fast enough that you will not notice a difference in normal pages, but the reasoning explains good habits: short selectors, no giant chains, and classes on the elements you actually want to style.
Class or ID?#
Use a class by default.
- Classes can be reused. IDs cannot.
- A class has low specificity, so it is easy to override. An ID is hard to override.
- A component you build today will probably appear twice tomorrow, and an ID cannot handle that.
IDs are right for in-page anchors (#pricing), form label targets, and JavaScript hooks. If you style an element by ID, you are borrowing trouble from your future self.
Common mistakes#
.cardvscard. The dot means class.cardwithout a dot is a type selector looking for a<card>element.- A stray space between two class names changing "and" to "inside".
- Duplicate IDs. HTML allows the page to load, but
querySelectorand anchors only find the first one, and your styles may apply to both. It behaves inconsistently. - Styling by tag inside components, like
.sidebar p, which then leaks into every paragraph in the sidebar later, including ones you add much later. - Forgetting quotes where needed:
[data-id=2]is invalid because2is not a valid identifier. Write[data-id="2"].
Practice#
- Write a stylesheet for a small form using only attribute selectors: style
[type="text"],[type="email"],[required]and[disabled]differently. - Add an arrow to every external link and a label to every link that ends in
.pdf, with no extra classes in the HTML. - Take a rule that uses an ID selector and rewrite it with a class. Note what you gain.
- Write a selector that matches
.btn.primaryand another that matches.btn .primary. Build HTML where only one of them matches.