Chapter 6 — CSS Foundations: Selectors and Cascade
Section 1 — What a selector does
A selector is the part of a CSS rule that picks which elements the rule applies to. In p { color: red; }, the selector is p, so every paragraph turns red. Without a selector, the browser would not know what to style. This sounds almost too simple to deserve a whole chapter, but selectors are the steering wheel of CSS: every visual decision you make begins with choosing exactly which elements should receive it, and choosing badly is the root of most CSS frustration. A selector that is too broad styles things you never meant to touch, and you then spend your afternoon writing extra rules to undo the damage. Learning selectors well means learning to aim precisely, and that precision pays off on every page you ever build.
Think of a stylesheet as a set of instructions addressed to particular elements. The selector is the address. Just as a letter addressed to "everyone in Lagos" reaches far more people than a letter addressed to one house, a selector like p reaches every paragraph on the page, while a selector like .warning reaches only the elements you deliberately marked with that class. The rest of the rule — the declarations inside the curly braces — is the message itself: what colour, what size, what spacing.
It helps to separate two questions whenever you write a rule. First, what do I want to change? That gives you the declarations. Second, which elements should change, and only those elements? That gives you the selector. Beginners often jump straight to the first question and grab the nearest selector that seems to work, then discover later that it also styled the footer, the sidebar, and a widget they had forgotten about. Getting into the habit of asking the second question first — and picturing exactly which elements will match — prevents a whole class of bugs.
There is also a practical reason selectors matter so much: they are how HTML and CSS connect. HTML describes what content is, and CSS describes how it looks, but the two files are separate, and the selector is the bridge between them. A well-chosen class name in HTML, paired with a clear selector in CSS, makes the relationship between structure and style obvious to anyone reading the code later — including you, three months from now, when you have forgotten why you wrote a rule.
Finally, remember that a selector does not change the HTML at all. It only describes a pattern, and the browser does the work of finding every element that fits that pattern. If you later add a new paragraph to the page, it automatically picks up the p rule without you touching the stylesheet. That automatic, pattern-based behaviour is what makes CSS scalable: a few dozen rules can style a site with hundreds of pages, because selectors match patterns rather than individual elements.
Check: In a CSS rule, what job does the selector do?
Section 2 — The anatomy of a rule
A complete CSS rule has three parts: a selector, a declaration block in curly braces, and one or more declarations inside it. Each declaration is a property, a colon, a value, and a semicolon, like color: blue;. Here is a rule with two declarations:
h2 {
color: blue;
font-size: 24px;
}
The selector h2 says "every level-two heading". The opening brace starts the declaration block, and the closing brace ends it. The last semicolon in a block is technically optional, but writing it anyway is a good habit, because you will almost certainly add another declaration later, and forgetting to add the semicolon then is a classic source of mysterious breakage. When a semicolon is missing, the browser cannot tell where one declaration ends and the next begins, so it may discard both.
It is worth slowing down over the vocabulary, because these words get used constantly. The property is the aspect of the element you are styling — colour, size, spacing, and so on. The value is the specific setting you want — blue, 24 pixels, centred. A declaration is one property-value pair. A declaration block is the whole group of declarations between the braces. A rule, sometimes called a rule set, is the selector plus its declaration block together.
A stylesheet is simply a collection of rules, usually in a separate .css file linked from the HTML. The browser reads the file from top to bottom, builds up a picture of how each element should look, and resolves conflicts using the cascade, which you will study from Section 22 onwards. One practical consequence is that a stylesheet is read in order, so the position of a rule in the file can matter — a point that becomes important once you understand specificity. Another consequence is that a single typo can break a whole rule or even a whole block: an unclosed brace, for instance, can swallow every rule that follows it, because the browser thinks they are all still inside the first block. When a large chunk of styling suddenly vanishes, an unclosed brace or a missing semicolon is one of the first things to check.
Formatting also matters, though the browser does not care about it. A rule can legally be written on one line — h2 { color: blue; font-size: 24px; } — and it works exactly the same. But multi-line formatting, with one declaration per line and consistent indentation, is far easier for humans to read, scan and edit.
Finally, note what a rule does not do: it does not create anything in the HTML, and it does not run in sequence like a program. CSS is declarative, which means you describe the desired result and the browser works out how to achieve it. You do not tell the browser "find the first heading, then change its colour, then move to the next one". You simply declare "headings are blue", and the browser applies that to every heading, now and whenever the page changes.
Check: Name the three parts of a CSS rule.
Section 3 — The element selector
An element selector targets every element of one type, using just the tag name. p { line-height: 1.6; } styles all paragraphs, and a { text-decoration: none; } styles all links. This is the simplest selector in CSS, and also one of the most useful, because many design decisions genuinely apply to a whole type of element: all body text should share a readable line height, all headings might share a font family, and all images might need a maximum width so they never overflow their container.
The strength of the element selector is its breadth, and its weakness is exactly the same thing. When you write p { color: #333; }, you are styling every paragraph on the page — the introduction, the article body, the footer note, and any paragraph inside a widget you add next month. That is fine when the decision really is universal, but it becomes a problem when different paragraphs need different treatments. At that point you need a more precise tool, such as a class, which you will meet in the next section. It is usually cleaner to use element selectors for genuinely global defaults and classes for everything that varies.
Element selectors are also the foundation of the browser's own default stylesheet. Before your CSS loads, the browser already styles h1 as large and bold, em as italic, and ul with bullets and indentation, all using the equivalent of element selectors. Your own element rules override those defaults because author styles — styles written by you, the author of the page — beat the browser's built-in user-agent styles in the cascade.
There are a few practical habits worth building around element selectors. First, keep them for base styles: typography defaults, link colours, image behaviour, list spacing. Second, be cautious about styling very generic elements like div or span. A div is used for almost everything structural on a modern page, so a rule targeting all divs will reach places you never intended, including third-party widgets. Third, remember that element selectors have the lowest specificity of any selector that names something, which is actually an advantage: a class rule can easily override an element rule later, so base styles stay easy to customise.
A worked example ties this together. Suppose you want all body text readable and all links a consistent colour. Two element rules do the whole job:
p {
line-height: 1.6;
color: #333;
}
a {
color: #0066cc;
}
Later, if one particular paragraph needs to be a warning, you add a class to that paragraph and a class rule to override just its colour, leaving the element rule as the default for everything else. That layered approach — broad defaults, narrow exceptions — is the pattern behind most maintainable stylesheets, and it starts with understanding exactly what an element selector reaches.
Check: What does the selector h3 target?
Section 4 — The class selector
A class selector starts with a dot and targets every element carrying that class. In HTML you write class="warning", and in CSS you write .warning { color: darkred; }. Unlike an ID, a class can be reused as many times as you like. Buttons, cards, warnings, highlighted notes and callouts are all natural classes, because a page typically contains several of each, and they should all look the same.
To use a class, you do two things. First, you mark the elements in HTML with the class attribute, using a name you choose: <p class="warning">Please save your work before closing.</p>. Second, you write a rule in CSS whose selector is a dot followed by that name: .warning { color: darkred; font-weight: bold; }. The dot is CSS's way of saying "this is a class name, not an element name". Without the dot, warning would mean an element called <warning>, which does not exist in HTML, and the rule would silently match nothing — a mistake that produces no error message, just a style that never appears, which is why it is worth double-checking the dot whenever a class style fails.
Class names are your choice, and choosing them well is a genuine skill. A good class name describes what the thing is or what it means, not what it currently looks like. .warning is better than .red-text, because a warning might be restyled in amber or orange one day, and then .red-text would be a lie that confuses everyone reading the code.
An element can carry more than one class, separated by spaces: <p class="card featured">. Each class contributes its own styles, and the element ends up with the combination. This is how you build variations without duplicating code: .card supplies the shared card styling — padding, border, background — while .featured adds only the extra touches that make a card stand out, like a coloured border. When two classes set the same property, the cascade decides the winner, using specificity first and source order second, as you will learn later in this chapter.
Classes also have a middle level of specificity — stronger than element selectors, weaker than ID selectors. That middle position is a feature, not an accident. It means a class rule can override a base element rule when you need an exception, and another class rule can override it in turn when you need a variation. Stylesheets built mainly from classes therefore stay flexible: nothing is so specific that it cannot be overridden without a fight, and nothing is so broad that it cannot be refined. This is why experienced developers reach for classes first for almost all styling, reserving IDs for the genuinely unique cases covered in the next section, and element selectors for global defaults. If you take one habit from this chapter, let it be this: when in doubt, use a class.
Check: How do you write a class selector for the class note?
Section 5 — The ID selector
An ID selector starts with a hash and targets the one element with that ID: #site-header { background: navy; }. Because an ID must be unique on a page, this selector should only ever match a single element. In HTML, you give an element its ID with the id attribute — <header id="site-header"> — and in CSS you refer to it with a hash followed by the name. The hash is the signal that you mean an ID, just as the dot signals a class, and confusing the two is a common early mistake that again fails silently: #warning matches nothing if the element was marked with class="warning".
Uniqueness is not just a convention; it is a rule of HTML. A page with two elements sharing the same ID is invalid, and browsers will still try to render it — they are forgiving by design — but the results become unpredictable. JavaScript's getElementById, for example, returns only the first matching element, so the second one becomes unreachable by that method. Fragment links like href="#contact" jump to the first element with that ID. If something appears more than once, it is a class, not an ID — that test settles almost every naming decision.
IDs have legitimate uses. They are ideal for the genuinely singular landmarks of a page: the site header, the main navigation, the main content area, the footer, a particular form. They are also essential outside CSS: fragment links jump to IDs, form labels connect to inputs through IDs, and JavaScript uses IDs to find specific elements quickly. The question is not whether IDs exist, but whether a given style rule should be tied to one — and the answer is usually no, because of specificity.
The problem with ID selectors in CSS is that they are extremely specific. An ID beats any number of classes, so a rule like #site-header { background: navy; } can only be overridden by another ID rule, an inline style, or !important. That makes the stylesheet brittle: the first time someone wants a variation — a different header background on one special page, for instance — they discover they cannot override the ID rule with an ordinary class, and they reach for !important or an even more specific selector, starting a spiral that makes the stylesheet harder and harder to change.
A practical rule of thumb, therefore, is this: use IDs in HTML for linking, labelling and scripting, but style with classes in CSS, even for unique elements. A header can be <header id="site-header" class="site-header">, with the ID available for links and scripts and the class carrying the styles. You lose nothing — the class selector still matches exactly that one element, since nothing else carries the class — and you gain the freedom to override the styles later with another class. In short: IDs identify, classes style, and keeping those jobs separate keeps your stylesheets flexible.
Check: Why should an ID selector only ever match one element?
Section 6 — The universal selector
The universal selector * targets every element on the page. It is mostly used for small, deliberate resets, such as * { box-sizing: border-box; }, which you will meet properly in Chapter 8.
The most famous legitimate use is the box-sizing reset. By default, CSS measures an element's width as its content only, with padding and borders added on top, which makes layout arithmetic fiddly. Setting box-sizing: border-box on every element makes width include padding and border instead, which is how most developers intuitively think about size. Applying it with * is a one-line way to change that behaviour everywhere at once, and it is so widely used that many developers paste it into every project without thinking.
Another occasional use is a quick reset of margins and padding: * { margin: 0; padding: 0; }. This strips the browser's default spacing from everything, giving you a blank slate. It works, but it is a blunt instrument: it removes useful defaults along with the unwanted ones, such as the indentation of lists and the spacing of paragraphs, and you then have to re-add spacing everywhere by hand. More targeted resets, which set defaults only for the elements that need them, are generally preferred today.
The danger of * is performance and unintended reach, though both are often overstated for small pages. Matching every element is more work for the browser than matching a class, but on a typical page the difference is negligible; the real cost is in what the rule does. A rule like * { color: red; } styles everything red, including elements you forgot about, and a rule like * { transition: all 0.3s; } animates every property change on every element, which can genuinely slow a page and produce bizarre visual effects. The universal selector also has zero specificity — it contributes nothing to a selector's score — which is actually convenient for resets, since any other rule can override it.
So treat * as a specialist tool. Use it for deliberate, page-wide resets where you genuinely want every element to behave the same way, and be explicit about what you are resetting and why. Avoid it for ordinary styling, where a class or element selector says what you mean far more clearly. A good test is to ask: if a new element were added to this page tomorrow — a widget, an embedded form, a third-party component — would I want this rule to apply to it automatically? For box-sizing, the answer is usually yes. For colours, fonts and spacing, the answer is almost always no. That question, applied honestly, will keep the universal selector in its proper place: powerful, occasionally indispensable, and used sparingly.
A final practical note: because * matches every element, it also matches elements inside the <head> in theory, though those are not displayed, so the practical effect is on visible content. And remember that * can be combined with other selectors — article * means every element inside an article, and *.warning is equivalent to .warning.
Check: What does the selector * match?
Section 7 — Grouping selectors
When several selectors share the same styles, group them with commas instead of repeating the rule. h1, h2, h3 { font-family: Georgia, serif; } gives all three heading levels the same font in one rule. Grouping is a small feature with an outsized effect on stylesheet maintainability: it expresses the idea "these different elements share this style" once, in one place, instead of scattering the same declarations across several rules that can then drift out of sync.
Consider the alternative. Without grouping, giving all headings the same font looks like this: three separate rules, each repeating font-family: Georgia, serif;. That works, but the moment you decide to change the heading font, you must find and change all three rules, and missing one produces a subtle inconsistency — two headings in the new font, one still in the old. With grouping, there is exactly one place to change, and the change applies everywhere at once.
The comma in a grouped selector is effectively an "or". The rule h1, h2, h3 { ... } means "apply to h1, or to h2, or to h3". Each selector in the group is evaluated independently, and an element needs to match only one of them to receive the styles. That independence matters when you mix selector types: h1, .subtitle, #intro { color: navy; } styles every h1, every element with class subtitle, and the element with ID intro. It also matters for specificity, because each selector keeps its own specificity score — a detail that becomes important when a grouped rule conflicts with another rule. The group as a whole does not have one combined specificity; each member is judged on its own.
Grouping has a failure mode worth knowing. If any selector in a group is invalid, the entire rule is discarded — not just the invalid member, but the whole group. So h1, h2, madeup-element { color: red; } is fine if madeup-element is a valid selector that simply matches nothing, but a genuinely malformed selector, such as one with a syntax error, invalidates the rule for every member.
There is also a judgement call in grouping: only group selectors that genuinely share a style for a reason, not merely by coincidence. Headings sharing a font family is a real design decision — they are a family of elements that should look related. A heading and a button that happen to both be navy today may diverge tomorrow, when buttons get a new brand colour and headings do not; grouping them now creates a false coupling that someone must untangle later.
Finally, grouping combines freely with everything else in this chapter. You can group complex selectors — article h2, section h2 { margin-top: 2em; } — and you can group selectors that use pseudo-classes or combinators. As stylesheets grow, grouping is one of the main tools for keeping them compact without losing clarity, and it pairs naturally with the habit of organising rules from general to specific, which you will return to near the end of this chapter.
Check: How do you apply one rule to both h2 and p?
Section 8 — The descendant selector
A space between two selectors means "an element inside another element". article p { color: #333; } styles paragraphs inside an <article>, but leaves paragraphs elsewhere alone. The inner element can be nested several levels deep and still match. This is the descendant combinator, and it is one of the most frequently used tools in CSS, because so much of styling is about context: a link inside navigation should look different from a link inside an article, a paragraph inside a card should be styled differently from a paragraph in the footer, and the descendant selector is how you express those contextual differences.
The key word is "inside", in the broadest sense. The descendant selector does not care how many levels deep the inner element sits, or what elements lie between the outer and the inner one. In article p, a paragraph directly inside the article matches, and so does a paragraph inside a <div> inside a <section> inside the article — as long as the article is an ancestor somewhere up the chain, the paragraph matches.
But that breadth is also the descendant selector's risk. A rule like div p matches paragraphs inside any div, which on a modern page is nearly all of them, since divs are the generic container. Overly broad descendant selectors are a common cause of styles "leaking" into places they were not intended for — a sidebar widget picking up article styles, for instance, because it happens to sit inside a div that also contains an article. The remedy is to anchor the outer selector to something meaningful, usually a class or a semantic element: article p is anchored to the article element, and .card p is anchored to the card class. Both say something about the context that justifies the style, rather than relying on a generic container.
Descendant selectors can chain to any length: main article section p means a paragraph inside a section inside an article inside a main element. Longer chains are more precise, but they are also more fragile, because they break if the HTML structure changes — add or remove a wrapper element, and the chain no longer matches. As a rule of thumb, two selectors are usually enough, and three should make you pause and ask whether a class on the inner element would be clearer and sturdier.
A concrete example shows the value. Imagine a page with a navigation menu and an article, both containing links. You want navigation links white on a dark bar, and article links blue and underlined. Element selectors alone cannot distinguish them, because both are <a> elements. Descendant selectors can:
nav a {
color: white;
text-decoration: none;
}
article a {
color: #0066cc;
text-decoration: underline;
}
Each rule is scoped to its context, and neither leaks into the other. This pattern — same element, different context, different style — is everywhere in real design, and the descendant selector is the standard way to express it.
Check: Does nav a style every link on the page, or only links inside a nav?
Section 9 — The child selector
The > combinator means "a direct child only". ul > li { margin-bottom: 8px; } styles list items directly inside a list, but not list items nested inside a deeper sub-list. The child selector is the descendant selector's stricter sibling: where the descendant selector asks "is this element inside that element at any depth?", the child selector asks "is this element immediately inside that element, with nothing in between?". That single distinction solves a whole family of bugs involving nested structures.
Lists are the classic example, because lists nest naturally. A navigation menu might contain a top-level list whose items each contain a sub-list of submenu items. If you style ul li — the descendant version — every list item at every level gets the style, including submenu items that were supposed to look different. If you style ul > li, only the top-level items are affected, and the submenu items are free to have their own rules. The same logic applies to any nested structure: comments with replies, menus with submenus, articles with nested sections. Whenever you catch yourself thinking "only the outer ones, not the inner ones", you are reaching for the child selector.
It is important to be precise about what "direct child" means. In HTML, elements nest inside each other like boxes inside boxes, and every element except the root has exactly one parent — the element directly containing it. A child selector matches when the element on the left is that parent. Text nodes, comments and whitespace do not count as elements, so they do not break the parent-child relationship: a paragraph containing only text is still a direct child of its containing div, even though the text sits between them in the source. What does break it is another element in between. In <ul><div><li>Item</li></div></ul>, the list item is a descendant of the list but not a child of it, because the div sits between them — though note that this particular nesting is invalid HTML anyway, since list items belong directly inside lists.
Child selectors can be combined with other selector types. .menu > li means list items directly inside an element with class menu, and nav > ul > li chains two child relationships to describe a precise path. As with descendant chains, long chains become brittle, so prefer short ones anchored to a meaningful class or element. And child selectors compose with grouping too: ul > li, ol > li { margin-bottom: 8px; } applies the same spacing to direct children of both kinds of list.
One more distinction is worth internalising, because it trips people up: the child selector is about structure, not about visual layout. Two elements can look like parent and child on screen — one box visually containing another — while being unrelated in the HTML, for instance when positioning moves an element over another. CSS selectors only ever see the HTML tree, never the visual arrangement. If your child selector is not matching an element that looks like it should match, check the actual HTML nesting in developer tools, not the visual layout. That habit — trusting the document tree over the picture — will serve you well throughout CSS, because the cascade, inheritance and layout all operate on the tree.
Check: What is the difference between div p and div > p?
Section 10 — The adjacent sibling selector
The + combinator targets an element that comes immediately after another element at the same level. h2 + p { margin-top: 0; } styles only a paragraph that directly follows a heading. Sibling selectors are about relationships between elements that share a parent, rather than parent-child relationships, and they are the tool for styling based on what comes before an element in the document. That turns out to be surprisingly useful, because documents are sequences: headings followed by paragraphs, labels followed by inputs, images followed by captions.
The word "adjacent" does all the work here. For h2 + p to match, three things must be true: the paragraph and the heading share the same parent, the paragraph comes after the heading in the document, and nothing else sits between them — no other element at all. If a <div>, an image, or even another paragraph intervenes, the match fails. Whitespace and text between the elements do not count, because they are not elements, but any element at all breaks adjacency. This strictness is what makes the selector precise: it expresses "immediately after", not "somewhere after", and that precision is exactly what you want when the style depends on immediate sequence.
The classic use case is spacing after headings. Browsers give headings a bottom margin and paragraphs a top margin, and when a heading is followed by a paragraph, those margins can stack into a gap larger than intended. A rule like h2 + p { margin-top: 0; } removes the paragraph's top margin only when it directly follows a heading, tightening that specific junction without affecting paragraphs elsewhere. In each case, the style is a response to the preceding element, and the adjacent sibling selector expresses that response directly.
Adjacent sibling selectors also shine in forms and interactive components. A label immediately before an input, an input immediately before an error message, a checkbox immediately before its custom-styled replacement — these are all "element B right after element A" patterns, and + describes them naturally. Combined with pseudo-classes, they become even more powerful: input:checked + label styles a label only when the checkbox before it is ticked, which is a standard technique for building custom checkboxes and toggle switches without JavaScript.
There are limits to keep in mind. Sibling selectors only look forwards, never backwards: there is no selector that styles an element based on what comes after it (with the exception of the newer :has() selector, which is beyond this chapter's scope). So h2 + p styles the paragraph because of the heading before it, but no selector styles the heading because of the paragraph after it. Sibling selectors also only match elements sharing a parent, so elements in different containers are never siblings, even if they appear adjacent on screen. Use the adjacent sibling selector where sequence genuinely matters, keep it short, and it will handle a surprising number of layout refinements elegantly.
Check: In h2 + p, what must be true about the paragraph's position?
Section 11 — The general sibling selector
The ~ combinator targets siblings that come after an element, not necessarily immediately after it. h2 ~ p { color: #555; } styles every paragraph that follows an h2 inside the same parent. If the adjacent sibling selector is about "immediately after", the general sibling selector is about "anywhere after, within the same parent". That looser condition makes it useful for a different set of problems: styling a whole run of elements that follow a trigger element, rather than just the first one.
To match h2 ~ p, a paragraph must share the heading's parent, must come after the heading in the document, and may have any number of elements between itself and the heading. In a section containing a heading followed by three paragraphs and a list, all three paragraphs match h2 ~ p, because all of them follow the heading inside the same parent.
This behaviour makes the general sibling selector good for section-level effects. Suppose you want everything after a particular heading in a section to be styled as part of that section's content — for instance, paragraphs following a "Notes" heading in a slightly smaller, greyer type. h2 ~ p expresses that in one rule, where the adjacent selector would only catch the first paragraph and leave the rest unstyled.
The general sibling selector shares the limitations of its adjacent sibling: it only looks forwards, and only within the same parent. Elements in a different container are never siblings, no matter how close they appear on screen, and an element cannot be styled based on a sibling that comes after it.
Choosing between + and ~ comes down to one question: does the style depend on being immediately next to the trigger, or merely on coming after it? Spacing adjustments usually depend on immediate adjacency — a paragraph's top margin only needs removing when it directly follows a heading. Section-wide styling usually depends on merely following — all paragraphs in a section after its heading belong to that section.
One caution: because ~ matches broadly, it can surprise you in long containers. A rule like h2 ~ p inside a page-level container matches every paragraph after the first heading on the entire page, which is almost certainly not intended. As with descendant selectors, anchoring matters: scope the rule to a meaningful container, such as section h2 ~ p or .notes h2 ~ p, so that its reach matches your intent. And remember that general sibling selectors, like all combinators, can be combined with classes, pseudo-classes and other combinators — input:checked ~ .panel is a well-known technique for showing a panel when a checkbox is ticked, and it works precisely because the panel need not sit immediately after the checkbox. Used with a clear sense of scope, the general sibling selector is a quiet workhorse of practical CSS.
Check: How does h2 ~ p differ from h2 + p?
Section 12 — The attribute selector
An attribute selector targets elements by an attribute they carry. a[target="_blank"] { color: purple; } styles only links that open in a new tab, and input[type="text"] targets text inputs specifically. Attributes are the extra information attached to HTML tags — href on a link, type on an input, alt on an image — and attribute selectors let you style based on that information rather than on element type or class alone. That is powerful, because attributes often carry meaning that classes would merely duplicate: a link's destination, an input's type, a form field's state.
The basic form is square brackets containing the attribute name, optionally followed by = and a value in quotes: [target] matches any element with a target attribute, whatever its value, while [target="_blank"] matches only those whose target is exactly _blank. The quotes around the value are good practice even when they are not strictly required, because values containing special characters need them, and consistency prevents errors. Note that the value match is exact and, for most attributes, case-sensitive: [type="TEXT"] does not match type="text", because HTML attribute values are generally case-sensitive even when the attribute names are not.
Attribute selectors are particularly valuable for forms. A form contains many <input> elements, but text inputs, checkboxes, radio buttons and submit buttons are very different things that deserve different styling. Without attribute selectors, you would need to add a class to every input to distinguish them. With attribute selectors, the type attribute already does that job: input[type="text"] for text fields, input[type="checkbox"] for checkboxes, input[type="submit"] for the submit button. The styling follows the meaning that is already in the markup, which keeps the HTML cleaner and the stylesheet honest — the style is attached to what the element is, not to a redundant label.
Links are another natural fit. External links, links to documents, and links that open in new tabs are all distinguishable by their href or target attributes, and the next section shows how partial matching extends this to patterns like file extensions. A common touch is an icon indicating that a link opens externally, applied with an attribute selector and a pseudo-element, without touching the HTML at all. That pattern — deriving presentation from existing semantic information — is one of the elegant ideas in CSS: the more meaning your HTML already carries, the less extra markup your styling needs.
Attribute selectors also work with custom data attributes, which authors can invent for their own purposes: <div data-status="error"> can be styled with [data-status="error"] { border-color: red; }. This is a legitimate and increasingly common technique for state-based styling, especially when JavaScript toggles the attribute value. It blurs the line between class and attribute somewhat, and classes remain the better default for most styling, but data attributes are the right tool when the value itself carries information — a status, a priority, a category — rather than merely marking membership in a group. As with other selectors, attribute selectors can be combined: a[href][target="_blank"] means a link with any href that also opens in a new tab, and input[type="text"].large combines attribute and class.
Check: Write a selector that targets only links with target="_blank".
Section 13 — Attribute selectors that match part of a value
Attribute selectors can match part of a value: [href^="https"] matches values starting with "https", [href$=".pdf"] matches values ending in ".pdf", and [href*="example"] matches values containing "example" anywhere. These three operators — ^= for "starts with", $= for "ends with", and *= for "contains" — turn attribute selectors from exact matching into pattern matching, which opens up a range of practical uses that exact matching cannot reach.
The symbols are worth memorising by association. The caret ^ points to the start, so ^= means "starts with". The dollar $ sits at the end of a line in many contexts, so $= means "ends with". The asterisk * is a wildcard in many systems, so *= means "contains anywhere". There is also a fourth operator, ~= for "contains this whole word in a space-separated list", which is how the class attribute itself works under the hood — [class~="warning"] is essentially what .warning means — but it is rarely written out by hand, since the class selector is shorter and clearer.
File types are the textbook example. A documentation site might link to PDFs, spreadsheets and zip files, and it is helpful to mark each kind differently, perhaps with a small icon. Rather than adding a class to every such link, you can style by extension: a[href$=".pdf"] for PDFs, a[href$=".zip"] for archives, and so on. The selector reads the information that is already in the href and acts on it, so a new PDF link added tomorrow is styled correctly without anyone remembering to add a class. The same logic applies to protocols: a[href^="https://"] matches secure links, while a[href^="mailto:"] matches email links, letting you distinguish link kinds by their destination pattern.
Partial matching is also useful for images and media. An image path often encodes useful information: img[src*="thumbnail"] matches thumbnails, and img[src$=".svg"] matches vector images. Combined with pseudo-elements or other properties, these selectors let you apply consistent treatment — sizing, borders, alignment — to categories of media that are already distinguished by their file paths. The key discipline is to rely on patterns that are stable: file extensions and directory names that your project controls are reliable, while patterns based on third-party URLs, which can change without warning, are fragile and should be avoided for critical styling.
There are caveats. Partial matching can over-match: [href*="pdf"] matches not only guide.pdf but also a page called pdf-guide.html and a domain containing "pdf", because "contains" does not know about word boundaries. When precision matters, prefer the more specific operator — $=".pdf" rather than *="pdf" — and test against realistic data. Also note that attribute value matching is case-sensitive by default, so [href$=".PDF"] does not match a lowercase .pdf link; CSS provides an i flag, as in [href$=".pdf" i], to make the match case-insensitive, which is handy for file extensions that authors write inconsistently. Finally, remember that these selectors match the attribute as written in the HTML, not the resolved URL: a relative link href="guide.pdf" matches $=".pdf" exactly as written, which is what you want, but a link written as href="/docs/guide.pdf?version=2" does not end in .pdf, because the query string is part of the value.
Check: Which attribute selector matches links to PDF files?
Section 14 — The hover pseudo-class
A pseudo-class describes a state an element can be in. a:hover { color: red; } applies while the pointer is over the link, and the style goes away when the pointer leaves. Pseudo-classes start with a single colon. The word "pseudo" means "false" — a pseudo-class is not a real class in the HTML, but it behaves like one: it marks elements that are currently in a particular state, and the browser applies or removes the class automatically as the state changes. You do not add hover to the HTML; the browser tracks the pointer and updates the matching in real time.
Hover is the most familiar pseudo-class, because it is the foundation of interactive feedback on the web. When a visitor moves the pointer over a button and the button darkens, or over a link and it underlines, that is :hover at work. The purpose is communication: hover tells the visitor "this is interactive, and you are pointing at it". That feedback is subtle but important, because without it, a page can feel dead — visitors are unsure what can be clicked, and they hesitate. A good hover style is noticeable but not dramatic: a slightly darker background, a colour shift, an underline appearing.
It is important to understand what hover is not. Hover is not a click, and it does not persist: the moment the pointer leaves, the style disappears, and the element returns to its normal appearance. Any information conveyed only by hover is therefore invisible to keyboard users and to touch-screen users, who have no pointer to hover with. The rule is: use hover for enhancement and feedback, never as the only way to reach content or information. A dropdown menu that opens on hover should also open on focus or tap, and a tooltip that appears on hover should have another way to be read.
Hover can be applied to any element, not just links — div:hover, li:hover, button:hover all work — though links and buttons are the most common cases. It also combines with other selectors in the usual ways: .card:hover styles a card when hovered, nav a:hover styles navigation links on hover, and button:hover:not(:disabled) styles a hovered button that is not disabled, showing how pseudo-classes chain together. The order of pseudo-class rules matters when they conflict: the classic guidance, sometimes remembered as "LoVe HAte", is to write link pseudo-classes in the order :link, :visited, :hover, :active, so that later states override earlier ones appropriately. If :hover is written before :visited, a visited link may not show its hover style, because the two rules are equally specific and the later one wins.
Hover is a seasoning, not the main ingredient, and the best interactive designs work without it and feel a little better with it.
Check: When does a:hover apply, and when does it stop applying?
Section 15 — The focus pseudo-class
:focus applies when an element has keyboard focus, such as a link you tabbed to or a form field you clicked into. Styling focus clearly, like a:focus { outline: 2px solid blue; }, helps keyboard users see where they are. Without a visible focus indicator, keyboard navigation becomes guesswork — the user presses Tab, nothing visibly changes, and they have no idea which link or button is about to be activated.
Browsers provide a default focus outline, usually a thin line around the focused element, but it is often subtle, and some designs deliberately remove it with outline: none because it clashes with the visual style. Removing the outline without providing a replacement is one of the most common accessibility mistakes on the web, and it effectively locks keyboard users out of confident navigation. The rule is simple to state and important to keep: every focusable element must have a visible focus state, always.
Focus applies to elements that can receive keyboard focus, which includes links, buttons, form controls, and any element given a tabindex attribute. Clicking an element with the mouse also gives it focus in most browsers, so mouse users see focus styles too, particularly on form fields — the highlighted border of a text input you clicked into is a focus style. The :focus-visible pseudo-class, a newer addition, refines this by applying only when the browser judges that a visible indicator is needed — typically for keyboard navigation rather than mouse clicks — allowing designs to show strong focus rings for keyboard users while keeping mouse clicks quieter. It is well supported in modern browsers and worth learning once :focus itself is comfortable.
Focus also interacts with the cascade in instructive ways. A focus rule and a hover rule for the same element are equally specific, so their order in the stylesheet decides which wins when an element is both hovered and focused — a common situation when a mouse user clicks a button. Writing :focus after :hover, or combining them in one rule like a:hover, a:focus { ... }, ensures keyboard and mouse users get consistent feedback. And focus styles should be tested with a keyboard, not just assumed: press Tab through your own page, watch where the focus goes, and check that every step is visible and logical. If focus disappears into an unstyled element, or jumps in a confusing order, that is a bug worth fixing before launch, not after.
Treating :focus as a first-class part of your styling, designed alongside hover rather than bolted on at the end, is one of the marks of a professional front-end developer. It costs a few lines, it harms nobody, and it opens the page to people who would otherwise be excluded.
Check: Why should you style :focus instead of removing outlines entirely?
Section 16 — First-child and last-child
:first-child matches an element that is the first child of its parent, and :last-child matches the last. li:first-child { border-top: none; } removes a top border from only the first item in a list. These are structural pseudo-classes, meaning they depend on an element's position among its siblings rather than on user interaction.
The key to understanding :first-child is that it describes the element itself, not its parent. li:first-child means "a list item that is the first child of its parent" — not "the first list item in the document", and not "the first child of a list". The distinction matters when siblings are mixed. Consider a container whose first child is a heading, followed by paragraphs: p:first-child matches nothing in that container, because the first child is a heading, not a paragraph, even though the first paragraph is the second child overall.
The classic use cases are edges and separators. When list items are separated by borders, the first item does not need a top border and the last does not need a bottom border, because there is nothing beyond them to separate from. Writing li:first-child { border-top: none; } and li:last-child { border-bottom: none; } handles those edges cleanly, and — crucially — it keeps working when items are added or removed, because the browser recalculates which item is first and last automatically. The alternative, adding special classes like first and last to the edge items by hand, breaks the moment the list changes, and it clutters the HTML with presentational information. Structural pseudo-classes derive the edge from the structure itself, which is exactly the kind of thing CSS is good at.
A similar pattern handles spacing. If paragraphs in an article each have a top margin, the first paragraph in the article will push away from the heading above it, creating a double gap. article p:first-child { margin-top: 0; } removes that gap for the first paragraph only — though note the subtlety above: if the article's first child is a heading rather than a paragraph, this rule matches nothing, and you would need a different approach, such as the adjacent sibling selector from Section 10 or the :first-of-type pseudo-class, which matches the first element of its type among siblings regardless of what comes before it. Knowing both :first-child and :first-of-type, and which one your situation needs, prevents a frustrating class of bugs.
First-child and last-child also combine naturally with other selectors. nav li:first-child a styles the link inside the first navigation item, .tabs li:last-child styles the last tab, and p:first-child::first-letter combines a structural pseudo-class with a pseudo-element to create a drop cap on the first paragraph's first letter. The habit to build is to ask, whenever you are tempted to add a "first" or "last" class by hand: can the structure itself tell the browser which element this is? Usually it can.
Check: In a list of five items, how many match li:first-child?
Section 17 — Nth-child
:nth-child() picks children by number or pattern. li:nth-child(2) picks the second item, li:nth-child(odd) picks the first, third, fifth and so on, and li:nth-child(3n) picks every third item. Where :first-child and :last-child handle the edges of a group, :nth-child() handles any position or repeating pattern within it, which makes it the tool for zebra striping, grid layouts, and any design where items alternate or repeat.
The argument inside the parentheses is an expression, and understanding that expression is the whole skill. A plain number picks exactly that position: :nth-child(3) is the third child. The keywords odd and even pick alternating children, starting with the first for odd and the second for even. And the formula form, written as an+b, picks every a-th child offset by b: 3n picks the 3rd, 6th, 9th and so on; 3n+1 picks the 1st, 4th, 7th; 2n is the same as even; -n+3 picks the first three children, because it counts backwards from three. The formula looks mathematical, but it is just a way of saying "start here, then step by this much", and a few minutes experimenting in the browser makes it intuitive.
Counting follows the same rule as :first-child: :nth-child() counts all sibling elements, not just siblings of the same type. In a container where a heading is the first child and paragraphs follow, p:nth-child(2) matches the first paragraph, because it is the second child overall. If you want to count only paragraphs, ignoring other element types, you need :nth-of-type(), which counts within the element type: p:nth-of-type(2) matches the second paragraph regardless of what else sits among the siblings. Mixing these two up is a very common source of "why is the wrong item highlighted?" bugs, so always ask yourself: am I counting positions among all children, or among children of this type?
The most celebrated use of :nth-child() is zebra striping — alternating row colours in a table or list, which help the eye track across a row without losing its place. tr:nth-child(even) { background-color: #f2f2f2; } colours every second table row, and it keeps working as rows are added or removed, unlike hand-applied classes. Similar patterns style every third item in a grid differently, highlight the middle item in a set, or remove the right margin from every third card in a three-column layout so the row ends flush. These are all patterns that would require tedious class management by hand, and :nth-child() expresses them as pure rules about position.
There are limits and cautions. Overusing :nth-child() for layout — for example, depending on an item always being third — makes the design fragile if content changes, because the styling is tied to position rather than meaning. When an item deserves special treatment because of what it is, a class is clearer than a position. Reserve :nth-child() for genuinely positional patterns: alternation, repetition, edges, and counts. And test with varying amounts of content: a pattern that looks perfect with six items may look odd with five or seven, especially when the last row of a grid is incomplete. With those cautions in mind, :nth-child() is one of the most satisfying selectors to learn, because it turns repetitive manual work into a single elegant rule.
Check: Which list items does li:nth-child(even) select?
Section 18 — The not pseudo-class
:not() excludes elements that match its argument. p:not(.intro) { color: #444; } styles every paragraph except those with the class intro. It is a handy way to say "everything except this". Most selectors describe what to include; :not() is the tool for describing what to leave out, and that inversion is useful more often than you might expect. Whenever you find yourself writing a rule for everything and then another rule to undo it for one exception, :not() lets you write the exception into the first rule instead.
The argument to :not() is itself a selector — a simple one in older browsers, and in modern browsers a list of selectors, so p:not(.intro, .warning) excludes paragraphs with either class. The exclusion applies to the element the :not() is attached to, not to its descendants or ancestors: p:not(.intro) means "a paragraph that does not itself have class intro", and a paragraph inside an .intro container still matches, because the paragraph itself is not intro. That precision matters, and misunderstanding it produces bugs where styles leak into nested content that the author thought was excluded.
A typical use is styling a group with one exception. Suppose all buttons on a page should have a certain margin, except the primary action button, which sits flush. button:not(.primary) { margin-right: 8px; } expresses that directly, without a second rule undoing the margin on primary buttons. Another common pattern is input:not([type="submit"]), which styles all form inputs except the submit button, avoiding the need to list every input type. And in navigation, li:not(:last-child) adds a separator or margin to every list item except the last, which is often cleaner than styling all items and then removing the separator from the last one with a separate rule.
Specificity with :not() deserves attention, because it is not what beginners expect. The :not() pseudo-class itself adds nothing to specificity, but its argument does: p:not(.intro) has the specificity of a paragraph plus a class, because the class inside the argument counts. So :not() does not give you a free way to write low-specificity exceptions; the exclusion still carries the weight of whatever it excludes. This is fair and consistent — a rule that mentions a class should count that class — but it surprises people who assume that a pseudo-class is "free". Keeping this in mind prevents specificity surprises when a :not() rule competes with another rule.
There is also a readability consideration. A :not() rule says what something is not, which is inherently harder to read than a positive statement about what it is. A stylesheet full of negations becomes difficult to reason about, because each rule requires you to hold both the included set and the excluded set in mind. Use :not() where the exception is genuinely the clearest way to express the intent — one exception to an otherwise uniform rule — and prefer positive classes where the groups are equally meaningful, such as .active versus .inactive. Like all powerful selectors, :not() is best used sparingly and deliberately, for the cases where "everything except this" is truly what you mean.
Check: What does button:not(:disabled) select?
Section 19 — Pseudo-elements for generated content
Pseudo-elements, written with a double colon, style a part of an element. ::before and ::after insert generated content before or after an element's real content, using the content property: p.note::before { content: "Note: "; font-weight: bold; }. Pseudo-elements differ from pseudo-classes in an important way: a pseudo-class describes a state of an existing element, while a pseudo-element creates or targets a part of an element, and in the case of ::before and ::after, creates a box that does not exist in the HTML at all.
The content property is what makes ::before and ::after produce anything visible. Without content, the pseudo-element generates no box at all, and the rule has no effect — a common beginner mistake is to style ::before elaborately and forget content, then wonder why nothing appears. The value of content is usually a quoted string, but it can also be empty (content: ""), which still generates a box that can be styled with dimensions and a background. That empty-content trick is the basis of many decorative techniques: a coloured bar beside a heading, a custom bullet, an underline effect, or an icon drawn with pure CSS shapes. The generated box becomes a decorative element that requires no extra HTML, keeping the markup clean while the presentation gains richness.
A classic, responsible use is labelling. The example above prefixes every note paragraph with the word "Note:" in bold, without the author having to type it in every paragraph. If the wording later changes to "Please note:", one CSS edit updates every note on the site. Similarly, a[href^="http"]::after { content: " ↗"; } can append an external-link indicator to external links, and blockquote::before { content: "“"; } can add a decorative quotation mark.
The names ::before and ::after can mislead. They do not mean "before the element" and "after the element" in the page; they mean "before the element's content" and "after the element's content", inside the element's own box. A ::before on a paragraph sits at the start of the paragraph, before its first word, and a ::after sits at the end, after its last word.
There are practical limits. Generated content is not part of the document, so users cannot select it as text in the usual way, search engines do not index it as content, and assistive technology largely ignores it — all reasons to keep it decorative. It also adds complexity: a page heavy with generated content becomes harder to debug, because the extra boxes are invisible in the HTML source and only appear in developer tools' element inspection. Use ::before and ::after where they genuinely reduce markup clutter or create a decoration that belongs to the styling layer, and prefer real HTML where the content matters. That distinction — decoration in CSS, meaning in HTML — is one of the guiding principles of good front-end work.
Check: Which property must ::before use to actually show anything?
Section 20 — First-letter and first-line
::first-letter styles the first letter of a block of text, which is how drop caps are made, and ::first-line styles only the first line. Both are useful for typographic touches without adding extra HTML. These are the other commonly used pseudo-elements, and unlike ::before and ::after, they do not generate new content: they select a part of the element's existing text and let you style that part differently. That makes them safe in a way generated content is not — the text remains real text, selectable, searchable and readable by screen readers, with only its presentation altered.
The drop cap is the signature use of ::first-letter. In printed books, the first letter of a chapter is often enlarged and decorated, drawing the eye into the text. The same effect in CSS is a few lines: p.intro::first-letter { font-size: 3em; float: left; line-height: 1; padding-right: 8px; font-weight: bold; }. The enlarged letter floats to the left, and the rest of the paragraph flows around it, reproducing the print effect without wrapping the letter in a separate element.
::first-line styles the first formatted line of a block, however long that line happens to be at the current width. That "however long" is important: the first line is not a fixed piece of text, but whatever fits on the first line after layout, so it changes when the browser window resizes or the font size changes. A rule like p::first-line { font-weight: bold; } therefore bolds a different number of words on a phone than on a desktop, which is usually the desired behaviour for a typographic lead-in — the opening phrase stands out regardless of screen size.
Both pseudo-elements have a subtle interaction with preceding content. ::first-letter applies to the first letter of the block, but if the paragraph begins with punctuation, such as an opening quotation mark, browsers include that punctuation in the first-letter styling, which is the typographically correct behaviour. And if a ::before pseudo-element generates content at the start of the paragraph, that generated content can affect which letter counts as first, depending on the browser.
The broader lesson of ::first-letter and ::first-line is that CSS can style parts of content without requiring authors to chop content into extra elements. That keeps HTML semantic — a paragraph remains a paragraph, not a paragraph plus a specially marked first letter — while still allowing fine typographic control. It is the same principle as generated content, applied to selection rather than creation, and it is one of the reasons CSS is a styling language rather than merely a colouring tool: it understands the structure of text well enough to address its parts. For your own pages, try a drop cap on an article introduction and a bold first line on a long feature: small touches, but they are the kind of detail that makes a page feel designed rather than merely assembled.
Check: Which pseudo-element would you use to make a drop cap?
Section 21 — Combining selectors
Selectors can be combined to get more precise. p.intro means "a paragraph with class intro", while p .intro (with a space) means "an element with class intro inside a paragraph". That single space changes the meaning completely.
When selectors are written without a space, they must all match the same element. p.intro requires the element to be a paragraph and to have class intro, both at once. You can stack as many conditions as you like on one element: p.intro.featured means a paragraph with both classes, input[type="text"].large means a text input with class large, and a:hover means a link in the hover state.
When selectors are written with a space, they describe a relationship between different elements: the second must be a descendant of the first. p .intro means an element with class intro somewhere inside a paragraph — a span inside a paragraph, for instance, or a link inside a paragraph. The two elements are different elements, and the space is the descendant combinator from Section 8. The same logic applies to the other combinators: p > .intro is an intro element directly inside a paragraph, p + .intro is an intro element immediately after a paragraph, and so on. The combinator always sits between selectors for two different elements.
The practical consequence is that reading a selector means reading it as a little sentence, paying attention to spaces. article p.intro reads as "a paragraph with class intro inside an article" — three conditions, but only two elements: the article, and the paragraph that is both a paragraph and intro. article p .intro reads as "an element with class intro inside a paragraph inside an article" — three elements, because there is a space before .intro.
Combining selectors also affects specificity, because every part of a compound selector contributes to its score. p.intro is more specific than .intro alone, because it adds an element to the class, and article p.intro is more specific still. That is usually what you want when you combine selectors to be precise — the precision should win over a vaguer rule — but it also means that over-combining makes rules harder to override later, as discussed in Section 40. A useful discipline is to combine only as much as the meaning requires: if .intro alone identifies the elements you mean, do not write p.intro just to be safe, because the extra element adds specificity you may regret when you need an exception.
li:first-child a:hover combines a structural pseudo-class, an element and a state pseudo-class across two elements to mean "a hovered link inside the first list item". These compound selectors read like precise descriptions of a situation, and that is exactly what they are. The more fluently you can read and write them, the more directly you can translate a design intention — "the link in the first item, when hovered" — into code, without adding classes to mark situations that the structure already describes.
Check: What is the difference between p.intro and p .intro?
Section 22 — What specificity means
Specificity is the browser's scoring system for deciding which rule wins when several rules target the same element and set the same property. More specific selectors beat less specific ones, regardless of which rule appears first.
Specificity exists because conflicts are inevitable. A page's stylesheet sets base styles for all paragraphs, component styles for cards, exception styles for featured cards, and state styles for hovered elements — and several of those rules will target the same element and the same property. The browser needs a deterministic way to decide which one wins, and "whichever comes last" alone is not enough, because authors reasonably expect a rule aimed precisely at one element to beat a broad rule that merely happens to include it. Specificity is that decision procedure: it ranks rules by how precisely they target the element, so that the most targeted rule wins, and only when two rules are equally targeted does their order in the file break the tie.
An element selector like p is a vague address — "every paragraph" — while a class like .intro is more precise, because it names a specific group the author deliberately marked, and an ID like #main is more precise still, because it names one unique element. The browser prefers the most precise address, on the reasonable grounds that a rule written for a specific thing probably intends to override a rule written for things in general. This model is not just an analogy; it is close to how specificity actually works, as the next section explains in detail.
It does not apply across different properties: a highly specific rule setting colour does not affect the font-size set by a less specific rule, because the two rules are not in conflict — each property is resolved independently. And it does not override inheritance in the way beginners sometimes expect: a directly targeted rule, however weak, beats an inherited value, however strong the parent's rule was, because inheritance is not a competing rule at all — it is a fallback that applies only when no rule targets the element directly. Section 33 returns to this point, because it is a frequent source of confusion.
Finally, specificity is not a goal in itself. Beginners sometimes treat it as a game to be won by writing the most specific selector possible, but that approach backfires, as Section 40 explains: every increase in specificity makes future overrides harder, until the stylesheet can only be changed with !important or even more specific selectors. The aim is not maximum specificity, but appropriate specificity — the least precise selector that correctly identifies the elements you mean, leaving room above it for exceptions and variations. Specificity is a tool for resolving conflicts predictably, not a weapon for winning them, and the best stylesheets are the ones where conflicts are rare because each rule is aimed at exactly the right level of precision.
Check: If two rules conflict, does the more specific or the less specific one win?
Section 23 — How specificity is scored
Think of specificity as three columns: IDs, classes (including pseudo-classes and attribute selectors), and elements (including pseudo-elements). A rule with one ID beats any number of classes, and a rule with one class beats any number of elements. Each selector is scored by counting how many IDs it contains, how many classes, pseudo-classes and attribute selectors it contains, and how many element names and pseudo-elements it contains, and those three counts are compared column by column, from left to right.
The column-by-column comparison is the crucial detail. Specificity is not a total: you do not add the columns together into one score. A selector with one ID — scored as (1, 0, 0) — beats a selector with a hundred classes — scored as (0, 100, 0) — because the comparison starts with the ID column, and 1 beats 0 there, regardless of what follows. Similarly, one class (0, 1, 0) beats a hundred elements (0, 0, 100), because the class column is compared before the element column.
Let us work through the counting for a few selectors, because practice makes the system automatic. p has no IDs, no classes and one element, so it scores (0, 0, 1). .intro has one class, so (0, 1, 0). p.intro has one element and one class, so (0, 1, 1). article p.intro adds another element, so (0, 1, 2). #main has one ID, so (1, 0, 0). a:hover has one element and one pseudo-class, and pseudo-classes count as classes, so (0, 1, 1). input[type="text"] has one element and one attribute selector, and attribute selectors count as classes, so (0, 1, 1) as well. The universal selector * counts as nothing in any column, so * scores (0, 0, 0), and combinators — spaces, >, +, ~ — also count as nothing, because they describe relationships, not elements. The :not() pseudo-class itself counts as nothing, but its argument does count, as Section 18 explained.
Inline styles and !important sit outside this three-column system. An inline style, written in a style attribute, beats any selector-based rule, no matter how specific, because it is attached to the element itself rather than matched by a pattern. And !important beats everything except another !important with higher specificity, which is why it is a last resort rather than a routine tool. Neither of these is a fourth column in the usual sense, but thinking of them as layers above the three columns — inline styles above selectors, !important above normal declarations — gives a workable picture of the full hierarchy.
The practical payoff of this system is predictability. When a style is not applying, you can score the competing selectors column by column and see which one should win, rather than shuffling rules around and hoping. Developer tools even show you the outcome: overridden declarations are crossed out, with the winning rule visible above them.
Check: Does a selector with ten element names beat a selector with one class?
Section 24 — Specificity examples
p has the lowest score here, .intro is higher, p.intro is higher still, and #main is higher than all of them. So for <p class="intro" id="main">, a colour set by #main wins over the same property set by .intro or p.
Start with the simplest contest. Two rules target the same paragraph: p { color: red; } and .intro { color: blue; }. The first scores (0, 0, 1) and the second (0, 1, 0). Comparing column by column, the ID columns tie at zero, the class column decides it — 1 beats 0 — so the paragraph is blue, regardless of the order of the rules in the file. Even if the p rule appears after the .intro rule, the class still wins, because specificity is consulted before source order. Order only matters when specificity is equal, which is the next example.
Now consider p { color: red; } and p { color: blue; } — two identical selectors. Their scores tie at (0, 0, 1), so source order decides, and the later rule wins: blue. This is the behaviour most beginners already know intuitively, and it is correct — but only for ties. The mistake is to generalise it into "later always wins", which fails as soon as specificity differs.
A more interesting contest involves combinations. Suppose a paragraph with class intro inside an article is targeted by article p { color: green; } scoring (0, 0, 2), by .intro { color: blue; } scoring (0, 1, 0), and by article p.intro { color: purple; } scoring (0, 1, 2). The first loses to both others, because its class column is zero while theirs is one. Between the remaining two, the class columns tie at one, so the element column decides: 2 beats 0, and the paragraph is purple. Notice that article p.intro wins not because it is longest, but because its class column ties with .intro and its element column breaks the tie. Length alone never wins; only the column counts do.
Finally, the ID example that motivates the "classes first" advice. A rule #main { color: red; } scores (1, 0, 0). To override it with a class-based rule, you would need another ID, an inline style, or !important, because no number of classes reaches the ID column. Suppose a designer wants a special version of the main content area on one page and writes .special #main — that scores (1, 1, 0), which does beat (1, 0, 0), because the ID columns tie and the class column decides. But now the override itself contains an ID, so overriding it again requires yet another ID, and the spiral continues. The lesson is not that IDs are evil, but that each ID in a stylesheet raises the floor for every future override, and a stylesheet full of IDs quickly becomes a place where only IDs can change anything. Classes keep the floor low and the stylesheet flexible.
Check: Rank these from weakest to strongest: #main, p, .intro.
Section 25 — The cascade, step by step
When rules conflict, the browser decides in this order: first it considers where the rule came from (its origin), then how specific it is, and finally, if all else is equal, which rule appears later in the stylesheet. Later beats earlier only when specificity is tied. This ordered decision procedure is the cascade itself — the "cascading" in Cascading Style Sheets — and understanding it as a sequence of steps, applied in order, is the key to predicting which rule wins any contest.
The full procedure is a little richer than the three steps above, because origin itself has layers. In order of increasing priority, the origins are: the browser's default styles (the user-agent stylesheet), the user's own styles if they have set any (a rare but real case, where a reader's custom stylesheet expresses preferences such as larger text), and your styles as the author. Within author styles, normal declarations are considered before !important declarations, which is why !important is so powerful: it jumps the queue within its origin. And within each of those layers, specificity is compared, and then source order. So the complete order is: origin first, then importance within origin, then specificity, then source order.
It helps to picture the cascade as a tournament with rounds. In the second round, the surviving rules are compared by specificity, and only the most specific survive. In the third round, if more than one rule is still standing — because they are equally specific — the one that appears latest in the stylesheet wins.
A concrete walkthrough makes this tangible. Imagine a paragraph targeted by four colour rules: the browser default (black), p { color: red; }, .intro { color: blue; }, and p.intro { color: green; }, in that order in the file. Round one eliminates the browser default, because author styles outrank user-agent styles. Round two compares specificities — (0, 0, 1), (0, 1, 0) and (0, 1, 1) — and p.intro wins outright, so round three is not needed. If instead the last two rules were .intro { color: blue; } and p.intro { color: green; } with the green rule removed, the survivors would be p and .intro, .intro would win round two, and again order would not matter. Only if the contest were between two rules with identical selectors, or different selectors with identical scores, would round three — source order — come into play.
The most common misunderstanding about the cascade is treating source order as the primary rule and specificity as an exception, when in fact the reverse is true: specificity is consulted first, and source order is only the tiebreaker. That inversion explains why moving a rule to the bottom of the file sometimes fails to make it win — it fails whenever a competing rule is more specific. Understanding the cascade as an ordered procedure, rather than a vague notion that "later and more specific wins", is what turns CSS debugging from trial and error into reasoning.
Check: If two equally specific rules conflict, which one wins?
Section 26 — Source order matters
Source order is why the order of your rules matters. If you write p { color: red; } and then later p { color: blue; }, paragraphs end up blue, because the two rules are equally specific and the later one wins. Source order is the final tiebreaker in the cascade, and although it is the last step in the decision procedure, it is far from unimportant.
The most important qualification is that source order only decides ties. If two rules have different specificity, the more specific one wins no matter where it appears, and moving the less specific rule to the end of the file changes nothing. Beginners often discover this the hard way: they add a new rule at the bottom of the stylesheet, fully expecting it to win because it is last, and nothing changes, because an earlier rule is more specific.
Source order also spans stylesheets, not just rules within one file. When a page links several stylesheets, they are treated as one long stylesheet in the order they are linked: rules in a later-linked file beat equally specific rules in an earlier-linked file. Controlling the link order in the HTML is therefore part of controlling the cascade, and it is worth checking whenever a third-party style unexpectedly wins.
Within a single stylesheet, thoughtful ordering makes the file self-documenting. A common and effective order is: first the base and reset styles that set defaults for the whole page, then layout styles for the major regions, then component styles for reusable pieces like cards and buttons, then state styles for hover and focus, and finally small overrides and utilities. This general-to-specific flow means that when you read the file top to bottom, you move from the broadest decisions to the narrowest, and when two rules tie, the later, narrower one is the one that should win. It also means that a developer adding a new override naturally places it near the end, where it has the best chance of winning ties — a convention that reduces surprises.
There is one more subtlety: source order is determined by where the rule appears in the final, combined stylesheet as the browser sees it, which includes rules inside @media blocks and other grouping constructs. A rule inside a media query that currently applies is treated as if it appeared at the position of its block in the file, so a media-query rule placed late in the file can override an earlier ordinary rule of equal specificity, but only when the media condition is true. This is exactly how responsive design works: base styles first, then media-query adjustments later that override them on certain screen sizes. Source order, in other words, is not a trivial detail but the mechanism that makes progressive enhancement and responsive refinement possible.
Check: Why do paragraphs end up blue in the example above?
Section 27 — Where a rule comes from
The browser itself has built-in default styles, called the user-agent stylesheet, which is why headings are bold and large even before you write any CSS. Your own stylesheet, the author stylesheet, overrides those defaults because author styles beat user-agent styles. Every rule the browser considers comes from somewhere, and that somewhere — its origin — is the first thing the cascade compares, before specificity or order are even looked at.
The user-agent stylesheet is the browser's own built-in set of rules. It is what makes an <h1> large and bold, a <ul> indented with bullets, an <em> italic, and a link blue and underlined, on a page with no CSS at all. They vary slightly between browsers — one browser's heading sizes differ a little from another's, and default margins are not identical everywhere — which is one reason stylesheets often begin with a reset or normalisation layer that brings the browsers into agreement before the author's own design starts.
Author styles are the rules you write, whether in a linked stylesheet, a <style> block, or an inline style attribute. In the cascade's origin ranking, author styles outrank user-agent styles, which is why a single rule like h1 { font-size: 32px; } replaces the browser's default heading size without any contest about specificity: origin is decided before specificity, so even the weakest author rule beats the strongest user-agent rule. A plain element rule is enough, because origin has already settled the matter.
There is a third origin that rarely appears but is worth knowing about: user styles. Some readers configure their own stylesheets, for accessibility or preference, such as forcing larger text or higher contrast. In the cascade, user styles sit between user-agent and author styles for normal declarations, meaning your author styles normally override them — a decision that has been debated, since it means a reader's accessibility preference can be overridden by a site's design. For !important declarations, however, the order reverses, and user !important rules beat author !important rules, giving readers a final say when they insist.
Finally, origins interact with the rest of the cascade in a specific sequence that is worth restating, because it summarises this whole part of the chapter. For each property on each element, the browser gathers every declaration that applies, discards all but those from the highest-priority origin, then discards all but the most specific among those, then takes the latest among any that remain. That is the cascade in one sentence, and every styling mystery you encounter can be traced to one of those three steps. When you know where a rule comes from, how specific it is, and where it sits in the file, you know why it wins or loses — and that knowledge is the difference between wrestling with CSS and directing it.
Check: Why is a heading bold before you write any CSS?
Section 28 — Important, and why to avoid it
Adding !important to a declaration, like p { color: red !important; }, forces it to beat almost everything else, including more specific selectors. It is a last resort, because it breaks the normal cascade and makes later overrides very hard. An !important declaration jumps ahead of all normal declarations from the same origin, regardless of specificity or order, so a humble element rule with !important beats an ID rule without it.
The immediate cost is that !important creates a new contest at a higher level. Once one declaration is !important, the only way to override it is with another !important declaration that is at least as specific, or with an inline !important style, or with a user !important rule. So the first !important invites a second, the second invites a third, and before long the stylesheet contains a layer of !important declarations competing among themselves, with the same specificity and ordering problems as before, only now harder to reason about because the normal cascade has been bypassed. Developers sometimes describe this as an arms race, and it is an apt metaphor: each escalation makes the next change harder, and the stylesheet gradually becomes a place where nothing can be changed without escalating further.
The second cost is to the people who maintain the stylesheet, including future you. A normal cascade can be reasoned about: score the selectors, check the order, find the winner. An !important declaration short-circuits that reasoning, so a developer debugging a style must first discover that !important is involved — which developer tools do show, but which is easy to miss in a large file — and then work out how to override it.
There are legitimate uses, and honesty requires naming them. Utility classes — single-purpose classes like .hidden or .text-center that are meant to win whenever they are applied — sometimes use !important, because their whole purpose is to override component styles reliably. Overriding third-party styles that you cannot change, such as a widget's inline styles, is another case where !important may be the only practical tool. And user stylesheets, as the previous section noted, use !important precisely because it gives readers the final say. These are all cases where the normal cascade is unavailable or inappropriate, and !important is the honest solution to a real constraint.
But those cases are exceptions, and the discipline is to treat them as such. Before reaching for !important, check the three ordinary explanations for a style not applying: is the selector correct, is a more specific rule winning, and is the stylesheet actually loaded? In the great majority of cases, one of those is the answer, and fixing it properly leaves the cascade intact for the next change. A stylesheet that uses it routinely is a warning sign that the author has stopped reasoning about specificity and started fighting the cascade — and fighting the cascade is a fight you can win today only by making tomorrow harder.
Check: Why is !important a bad habit even though it "works"?
Section 29 — Inheritance
Some properties are inherited: if you set them on a parent, children pick them up automatically unless they set their own value. body { color: #222; font-family: Arial, sans-serif; } gives the whole page that text colour and font in one rule. Inheritance is the quiet mechanism behind much of CSS's efficiency. With it, a handful of rules on high-level containers — usually body — establish defaults that flow down through the entire document tree, and only the exceptions need their own rules.
The properties that inherit are mostly the text-related ones: color, font-family, font-size, font-weight, font-style, line-height, letter-spacing, text-align, visibility, and a few others. If you set a font on a container, you are really saying "the text in this region uses this font", and every descendant text element honours that unless it has a reason not to. That is why setting typography on body is the standard starting point for a page's design: it establishes a baseline for everything, in one place, with one rule.
Inheritance flows through the document tree, parent to child, one level at a time, and it can pass through many levels unchanged. A font set on body reaches a paragraph inside a section inside an article inside a main element, because each level passes the value down to the next, unless some intermediate level sets its own value, in which case the descendants below that level receive the new value instead. Inheritance is therefore both global and local: global in that defaults spread everywhere, local in that an override is contained to its own branch of the tree.
It is crucial to distinguish inheritance from the cascade, because beginners often conflate them. The cascade decides which rule wins when several rules target the same element directly. Inheritance applies only when no rule targets the element for that property at all — it is the fallback, the value an element receives by default from its parent, not the result of a contest. This distinction is why a directly targeted rule, however weak, always beats an inherited value, as the next sections explore: the cascade is consulted first, and inheritance only fills the gaps it leaves.
A practical consequence is that inheritance lets you write less CSS and change designs faster. Want to change the site's body font? Change the body rule, and every element that inherits it follows. Want a dark section with light text? Set color on the section, and all its text follows, without touching each paragraph. The pattern is to set text properties high in the tree, at the level where the decision belongs, and let inheritance carry them down. When you find yourself repeating the same font or colour rule on many elements, that is a sign the rule belongs higher up, on a shared ancestor, where inheritance can do the repetition for you.
Check: If you set color on body, do paragraphs inside it inherit it?
Section 30 — Not everything inherits
Text-related properties like colour, font and line height inherit, but layout properties like margin, padding, border and background do not. A parent's padding does not spread to its children — each element has its own box, as Chapter 8 explains. Text properties describe the content's appearance, and content inside content should look consistent. Layout properties describe an element's own box — its spacing, its border, its background — and if those spread to children, every nested element would accumulate its ancestors' spacing and borders, producing a mess that nobody could predict or control.
Consider what would happen if margins inherited. A container with a 20-pixel margin, containing a section with a 20-pixel margin, containing a paragraph, would give the paragraph 40 pixels of margin from its ancestors alone, plus its own, and every level of nesting would push content further and further inward for no reason related to the design. By not inheriting layout properties, CSS ensures that each element's box is its own responsibility: you set a box's spacing and background on that box, and nested boxes start clean.
There is a subtlety with backgrounds that is worth clarifying, because it confuses people. A child element with no background of its own appears to show its parent's background, but that is not inheritance — it is transparency. The child's background is transparent by default, so you see through it to whatever is behind it, which is the parent's background. If you give the child its own background, it covers the parent's completely.
The full list of inheriting properties is defined by the CSS specification for each property, and you do not need to memorise it — a good reference or developer tools will tell you whether a given property inherits. But the pattern is reliable enough to guide intuition: if a property describes text or the general character of content (colour, font, line height, letter spacing, text alignment, visibility, cursor), it probably inherits. If it describes a box or its position (margin, padding, border, width, height, background, display, position), it probably does not. When you are unsure, test it: set the property on a parent, inspect a child in developer tools, and see whether the child shows the value as inherited or as its own initial value.
This division also explains a common beginner puzzle: setting a font on body styles the whole page, while setting a margin on body only moves the body itself. Both are single rules on the same element, but font inherits and margin does not, so their effects spread differently. Once you internalise that split, many "why did this spread but that didn't?" questions answer themselves. And it connects to a design principle: put inheriting properties high in the tree, where they can spread usefully, and put non-inheriting properties on the specific element they belong to, where they stay contained. That placement — text decisions high, box decisions local — is the shape of a well-organised stylesheet.
Check: Does a parent's margin pass down to its children?
Section 31 — The inherit keyword
The inherit keyword forces an element to take its parent's value for a property, even one that would not normally inherit. a { color: inherit; } makes links match the surrounding text colour instead of the browser's default link colour. The inherit keyword is a way of opting into inheritance on demand: for any property, on any element, writing inherit as the value says "use whatever my parent computed for this property".
Links are the classic example, because they illustrate a real design tension. Browsers give links their own default colour — traditionally blue, and purple once visited — through the user-agent stylesheet. That default is a directly targeted rule, so it beats inheritance: a link inside a dark footer does not inherit the footer's light text colour, because the browser's link rule targets the link directly and wins over the inherited value. The result is blue links on a dark background, which may be unreadable or simply off-brand. Setting a { color: inherit; } overrides the browser default with an explicit instruction to inherit, so links take on the surrounding text colour wherever they appear — dark text in the article, light text in the footer — and the design stays consistent without a separate link colour for every region.
The same technique solves other problems. A button inside a card might need the card's border colour, a form control might need its container's font, or a nested element might need a parent's custom spacing. In each case, inherit expresses the intent directly: "be like your parent for this property".
There are a few things to understand about how inherit resolves. It takes the parent's computed value — the final value after the parent's own cascade and inheritance have been resolved — not the value written in the parent's rule. So if the parent's colour came from inheritance itself, the child inherits that same inherited value, and the chain continues up the tree until it reaches an element with a directly set value or the document root. If the parent has no computed value because the property does not apply, or if the element has no parent, inherit falls back to the property's initial value. And inherit participates in the cascade like any other value: a rule setting color: inherit competes with other colour rules for the same element by specificity and order, and it wins or loses like any declaration.
Whenever you find yourself writing a rule whose value duplicates a parent's value, pause and ask whether inherit says what you mean more honestly. Often it does, and the stylesheet becomes shorter and more resilient as a result. It is a small keyword, but it embodies a useful way of thinking about CSS: instead of telling every element exactly what to be, tell some elements to be like their surroundings, and let the structure of the page do the work of keeping the design coherent.
Check: How would you make a link use its parent's text colour?
Section 32 — Initial and unset
The initial keyword resets a property to the browser's default value, and unset acts like inherit for inherited properties and like initial for the rest. Both are escape hatches for undoing a style you no longer want. Writing initial as a value explicitly requests that default, discarding any inherited value and any value from earlier rules that this declaration overrides.
Why would you want to reset to a default? The common case is undoing a broad rule for a specific element. Suppose a stylesheet sets all links to a brand colour, but one particular region — a footer with its own design, or a third-party widget — needs links to look like plain browser links again. Writing footer a { color: initial; } restores the browser default for those links, without you needing to know or restate what that default is. The keyword is a way of saying "forget what my ancestors and earlier rules said; go back to the beginning".
unset is a smarter version of the same idea: it behaves like inherit if the property normally inherits, and like initial if it does not. That makes it the right choice when you want to "undo" a style without thinking carefully about whether the property inherits. For a text property like color, unset gives you the parent's colour — the natural, inherited state. For a layout property like margin, unset gives you zero — the natural, uninherited state. In both cases, unset returns the element to the state it would have been in if the overriding rule had never existed, which is usually what "undo this style" actually means.
These keywords are rarely needed in small stylesheets, and that is fine — they are tools for specific situations, not everyday vocabulary. But they become valuable as stylesheets grow and interact with third-party code, where you may need to carve out an exception from a broad rule without knowing every value involved. That explicitness is useful documentation, too: a rule that says color: unset tells the next developer "this element deliberately does not participate in the surrounding colour scheme", which is more informative than a rule that happens to set a colour that looks like the default.
One caution: initial and unset are blunt. They discard the inherited value entirely, which may not be what you want if the parent's value was itself a deliberate design choice that should apply everywhere except for one aspect. In such cases, a targeted override — setting the specific value you want — is clearer than a reset. Use the reset keywords when you genuinely want to step outside the surrounding design, such as for embedded third-party content or for a component that must look the same regardless of its context, and use ordinary overrides when you want a variation within the design. Knowing both options, and choosing between them deliberately, is part of writing CSS that other people can understand.
Check: What does unset do for a property that normally inherits?
Section 33 — Specificity beats inheritance
A rule that directly targets an element always beats a value that element merely inherited from a parent, no matter how specific the parent's rule was. Inheritance is the weakest way a style can reach an element. This is a point that deserves emphasis, because it contradicts a plausible intuition: if a parent is targeted by a very specific rule, such as an ID selector, you might expect that specificity to flow down to its children and overwhelm their own weaker rules. It does not. Specificity is a property of the contest between rules targeting the same element, and an inherited value is not a rule targeting the child at all — it is a fallback that applies only when no rule targets the child for that property.
Consider a concrete example. A container with ID main sets its colour to red: #main { color: red; }. Inside it sits a paragraph targeted by a plain element rule: p { color: blue; }. The paragraph is blue, not red, even though the ID rule is far more specific than the element rule. Why? Because the two rules are not competing for the paragraph. The ID rule targets the container, and it wins the contest for the container's colour. The element rule targets the paragraph, and it wins the contest for the paragraph's colour — a contest in which the ID rule does not participate, because it does not target the paragraph. Inheritance would give the paragraph the container's red only if the paragraph had no colour rule of its own, but it does, so inheritance never comes into play.
By making inheritance the weakest mechanism, CSS ensures that local rules always have the final say about their own element, and that containers can set defaults without dictating details. A section can set a default text colour for its content, and any paragraph inside it that needs a different colour can simply set its own, with a plain element or class rule, without fighting the section's specificity.
The practical rule that follows is simple: when an element's style is not what you expect, check first whether a rule targets that element directly, before blaming inheritance. Developer tools make this visible: the Styles panel shows the rules targeting the selected element, and the Computed panel shows the final value and whether it came from a rule or from inheritance. If the computed value is inherited, no direct rule won, and you can add one. If a direct rule is winning, inheritance is irrelevant, and you need to beat that rule's specificity or order.
There is one more layer to this that ties the chapter together. The cascade's full order for a given property on a given element is: first, rules that directly target the element, resolved by origin, specificity and order; and only if no such rule sets the property, the inherited value from the parent; and only if there is no inherited value either, the property's initial value.
Check: A parent has an ID selector setting its colour; a child has a plain element selector setting its own colour. Which colour does the child show?
Section 34 — Classes first, IDs sparingly
Because IDs are so specific, they are hard to override later — you need another ID or !important to beat one. Classes give you a middle level of specificity that is easy to override with one more class, which is why most stylesheets are built mainly from classes. This section gathers the specificity arguments from earlier sections into a practical guideline that professional developers follow almost universally: style with classes by default, use element selectors for base defaults, and reserve ID selectors for the rare cases where you genuinely want a style to be difficult to override.
The core argument is about the cost of change. A stylesheet is not finished when it first works; it is changed repeatedly as designs evolve, features are added, and exceptions accumulate. Each change needs to override some existing rule, and the ease of that override depends on the existing rule's specificity. If existing rules are classes, an override needs at most another class, or a class combined with a pseudo-class or an element — all modest, readable selectors. If existing rules are IDs, an override needs an ID, which means the HTML must carry that ID, and the next override needs yet another ID, and the stylesheet's specificity ratchets upward with every change.
Classes also compose better than IDs. An element can carry many classes, and each class contributes a layer of styling that can be added, removed or swapped independently. A card can be .card, .card.featured, or .card.compact, with each variation expressed as an additional class that overrides only what it needs to. An element has only one ID, so ID-based variations cannot compose this way; each variation would need its own ID and its own rule, duplicating the shared styles. The composability of classes is what makes component-based design possible in CSS: small, reusable, combinable pieces that build up into complex interfaces without duplication.
Class-based styles with modest specificity are forgiving: two developers can each write class rules, and conflicts are resolved predictably by order and by small specificity differences. ID-based styles are territorial: an ID rule claims its element absolutely, and another developer's attempt to adjust that element from a different component becomes a specificity battle.
None of this means IDs are useless — Section 5 explained their real roles in linking, labelling and scripting, and those roles are important. The guideline is specifically about styling: keep IDs in HTML for their non-styling jobs, and style through classes even for unique elements, by giving the unique element a class as well. A header can be <header id="site-header" class="site-header">, with the ID serving links and scripts and the class carrying the styles. When you do encounter an ID selector in a stylesheet, treat it as a signal to pause and ask whether the style truly needs to be that hard to override, or whether a class would serve just as well. Usually, a class serves just as well.
Check: Why do experienced developers style mostly with classes rather than IDs?
Section 35 — Inline styles
A style attribute on an element, like <p style="color: red;">, beats nearly every stylesheet rule, because it is attached to the element itself. Use it only for one-off exceptions, since it cannot be reused and is hard to find later. Inline styles are the highest-priority normal declarations in the cascade: they outrank any selector-based rule, no matter how specific, because the style is written on the element itself rather than matched by a pattern. That power is precisely why inline styles should be rare.
The legitimate uses of inline styles are narrow but real. They are the mechanism by which JavaScript most commonly sets styles dynamically: when a script needs to position an element at a calculated coordinate, or set a width based on user input, it writes an inline style, because the value is not known until runtime and cannot be written in a static stylesheet. In each of these cases, the inline style is generated or required by circumstances outside the author's ordinary control, not chosen as a styling method.
For ordinary, hand-written styling, inline styles have serious drawbacks. The first is reusability: a style written inline applies to exactly one element, so a design decision that should apply to ten elements must be written ten times, and changing it means finding and editing all ten. The second is discoverability: a stylesheet gathers a page's design in one place where it can be read, searched and reviewed, while inline styles are scattered through the HTML, mixed in with content and structure, where they are easy to overlook.
There is also a separation-of-concerns argument that matters for larger projects. HTML describes content and structure, CSS describes presentation, and keeping them in separate files lets each be edited, reviewed and cached independently. A designer can restyle a site by editing the stylesheet without touching the HTML, and a content editor can change text without risking the design. Inline styles blur that boundary, embedding presentation in the content file, so every design change becomes a content edit and every content edit risks a design change. For a one-page experiment, that hardly matters. For a site maintained over years by several people, it matters a great deal.
So the guideline is: use stylesheets for styling, and reserve inline styles for values that are genuinely dynamic or imposed from outside. When you are tempted to add a quick inline style to fix a single element, consider whether a class would serve instead — a class like .highlight can be applied to that element today and reused tomorrow, and it keeps the design in the stylesheet where it belongs. If the fix is truly a one-off, a one-off class is still usually better than an inline style, because it is findable and overridable. Inline styles are a powerful escape hatch, and like !important, their power is best preserved by using them only when no ordinary mechanism will do.
Check: Does an inline style beat a class rule in a stylesheet?
Section 36 — One element, many classes
An element can carry several classes at once: <p class="card featured">. Each class adds its own styles, and when two classes set the same property, the normal cascade rules decide — specificity first, then source order. Multiple classes are one of the most practical ideas in CSS, because they let you compose an element's appearance from small, reusable pieces instead of creating a new, monolithic class for every variation.
The composition pattern works like this. A base class supplies the shared styling: .card gives padding, a border, a background and rounded corners — everything that makes a thing a card. A modifier class supplies only the difference for a variation: .featured adds a coloured border and a subtle shadow, .compact reduces the padding, .dark swaps the colour scheme. An element that needs to be a featured card carries both classes, <div class="card featured">, and receives the combination. An element that needs to be a compact dark card carries three.
When two classes on the same element set the same property, the cascade resolves the conflict exactly as it resolves any other: compare specificity, then source order. Two single classes have equal specificity — each scores (0, 1, 0) — so source order decides, and the class whose rule appears later in the stylesheet wins. Note that the order of classes in the HTML attribute does not matter at all: <div class="card featured"> and <div class="featured card"> are identical, because the HTML order is not part of the cascade. Only the stylesheet order counts.
That ordering rule has a design implication: modifier classes should generally appear after base classes in the stylesheet, so that when they conflict, the modifier wins, which is the behaviour you want — a featured card's border should override the plain card's border. Stylesheet organisation therefore matters for multiple classes in a way it does not for single classes: grouping base classes first and modifiers later is not just tidiness, it is the mechanism that makes modifiers work. Some methodologies formalise this further by using compound selectors for modifiers, like .card.featured, which scores (0, 2, 0) and therefore beats the base class on specificity regardless of order.
Multiple classes also interact with JavaScript in useful ways. Scripts commonly add or remove a single class to change an element's state — adding .open to a menu, .active to a tab, .hidden to a dismissed notice — while leaving the element's other classes untouched. Because classes are independent, toggling one does not disturb the others, and the element's base styling persists through state changes. This is the standard technique for CSS-driven interactivity: JavaScript manages which classes are present, and CSS defines what each class looks like, keeping behaviour and presentation in their proper layers. An element with many classes is therefore not a messy element; it is an element whose appearance is described as a combination of independent, meaningful parts, which is exactly how complex interfaces should be built.
Check: In <p class="card featured">, which class's colour wins if both set one?
Section 37 — Styling by state, not just by type
Pseudo-classes let you style elements by what is happening to them: :hover for the pointer, :focus for keyboard focus, :checked for a ticked checkbox, :disabled for a greyed-out control. This is styling behaviour, not just appearance. Up to this point, most selectors in this chapter have described what an element is — its type, its class, its position in the document. Pseudo-classes add a new dimension: what state the element is in right now, a dimension that changes over time as the user interacts with the page.
Each state pseudo-class corresponds to a condition the browser tracks. :hover tracks the pointer, :focus tracks keyboard focus, :active tracks the moment of being pressed, :checked tracks whether a checkbox or radio button is ticked, :disabled and :enabled track whether a form control can be used, :required and :optional track whether a field must be filled, and :valid and :invalid track whether a field's value passes its constraints. These states are not set by the author; they are maintained by the browser as the user interacts, and the stylesheet merely describes how each state should look.
State styling serves two purposes: feedback and communication. Feedback confirms an action: a button darkens when pressed (:active), a field glows when focused (:focus), a checkbox shows a tick when checked (:checked). Communication conveys information: a disabled button looks greyed out (:disabled) so users know not to try it, an invalid field gets a red border (:invalid) so users know something needs fixing, a required field is marked so users know it cannot be skipped. In both cases, the style is not decoration; it is part of the interface's language, telling the user what is happening and what is possible.
The combination of state pseudo-classes with other selectors is where the real power lies, and it enables a surprising amount of interactivity without JavaScript. The pattern input:checked + .panel shows a panel when a checkbox is ticked, which can build tabs, accordions and toggles in pure CSS. The pattern form:invalid button[type="submit"] can style a submit button differently while the form is invalid. These combinations read as descriptions of situations — "a hovered button that is not disabled" — and they let the stylesheet respond to the full situation, not just to one aspect of it.
There are cautions, as with hover. State styles must never be the only way information is conveyed, because not every user experiences every state: touch users do not hover, keyboard users experience focus differently from mouse users, and screen reader users may not perceive a purely visual state change at all. Pair visual state styles with textual cues where the information matters — an error message in words alongside the red border, a label change alongside a toggle's colour shift. And test states deliberately: tab through a form, tick and untick checkboxes, submit invalid data, and watch whether each state is visible and sensible. State styling is a small investment that pays off in every interaction a visitor has with your page.
Check: Which pseudo-class styles a checkbox that is currently ticked?
Section 38 — When two selectors look alike
ul li matches list items at any depth inside a list, while ul > li matches only direct children. Choosing the wrong one is a common bug: styles "leak" into nested lists when you meant only the outer one.
The nested-list leak is the archetype. A page has a navigation menu: a top-level list of sections, each containing a sub-list of pages. The author wants top-level items large and bold, and sub-items smaller and indented. Writing nav li { font-size: 18px; font-weight: bold; } styles every list item at every level, because the descendant combinator reaches through nesting, so sub-items inherit the large bold treatment and then need their own rule to undo it. Writing nav > ul > li { ... } styles only the top level, leaving sub-items free for their own rule.
Similar near-misses abound. p.intro versus p .intro, covered in Section 21, differs by a single space and means "a paragraph that is intro" versus "an intro element inside a paragraph". .card p versus .card > p differs by a > and means "any paragraph in a card" versus "a paragraph directly in a card, not inside a nested section of the card". a:hover versus a :hover differs by a space and means "a hovered link" versus "a hovered element inside a link", which is a different thing entirely and usually matches nothing, since links rarely contain hoverable children. And li:first-child versus li:first-of-type differs in whether other element types among the siblings count. In each case, the two selectors look alike at a glance, and only careful reading — or a failing test — reveals the difference.
The defence against these bugs is a combination of habits. First, read selectors aloud as sentences, translating each combinator into words: "a paragraph inside an article", "a list item directly inside a list", "a paragraph that has class intro". If the sentence does not match your intention, the selector is wrong, however plausible it looks. Second, test with nested and varied content, not just with the simplest case: a rule that works on a flat list may fail on a nested one, and the nested case is where the bug lives. Third, use developer tools to inspect an element that is wrongly styled or unstyled, and read the list of matching rules: the offending rule is usually visible there, and its selector, read carefully, reveals the near-miss.
A class on the top-level items, like .nav-item, says exactly what you mean without depending on the combinator at all, and it survives restructuring in a way that a descendant or child selector does not. Combinators are powerful and precise, but they tie styling to structure, and structure changes. Classes tie styling to meaning, which changes less often. The best stylesheets use both, choosing combinators where the structural relationship is the point — a paragraph inside an article really is defined by its context — and classes where the meaning is the point.
Check: Why might a nested sub-list unexpectedly pick up the outer list's styles?
Section 39 — Reading the cascade in developer tools
In the browser's developer tools, the Styles panel shows every rule that applies to the selected element, with overridden declarations crossed out. If your style is crossed out, a more specific or later rule is winning — and the panel shows you which one.
To use this, open developer tools — usually with F12, or by right-clicking an element and choosing Inspect — and select the element you are interested in, either by clicking it in the page with the element picker or by finding it in the Elements tree. The Styles panel, typically on the right, lists every rule that matches the element, ordered from most to least priority, with the element's own inline styles first, then matched rules in cascade order. Beside each rule it shows the stylesheet and line number it came from, so you can jump to the source. Declarations that lost the cascade contest are shown crossed out, with the winning declaration visible in the rule above them.
The Computed panel complements the Styles panel. It shows the final, resolved value of every property for the element, after the cascade, inheritance and initial values have all been considered. Expanding a property there shows the chain of values that competed for it, including whether the winning value came from a rule or from inheritance. Between the Styles panel (which rules competed) and the Computed panel (what won and why), you have a complete account of the cascade for any element.
A practical debugging workflow follows naturally. When a style is not applying, inspect the element and look for your rule in the Styles panel. If it is absent, the selector does not match — check the spelling, the combinators, and the HTML structure. If it is present but crossed out, another rule is winning — read the rule above it, compare specificities, and decide whether to adjust your selector, reorder the rules, or accept that the other rule should win. If it is present and not crossed out but the element still looks wrong, the problem is elsewhere: the property value may be invalid, the property may not apply to this kind of element, or another property may be interfering. This workflow replaces guessing with evidence, and it works for every cascade problem, from the simplest to the most tangled.
You can also experiment live: edit a declaration in the Styles panel, add a new rule, toggle a declaration off, and watch the page update instantly, without saving anything. That live experimentation is the fastest way to build cascade intuition, because it closes the loop between a change and its effect immediately. Make developer tools a habit, not a last resort, and the cascade will stop feeling like an adversary and start feeling like a system you can read.
Check: In developer tools, what does a crossed-out declaration mean?
Section 40 — Specificity is not a competition to win
Writing the most specific selector you can think of feels safe, but it paints you into a corner: the next change needs something even more specific. Aim for the least specific selector that does the job, so future changes stay easy. That fix works in the moment, and it feels decisive, but it mortgages the future: every increase in specificity raises the bar for the next override, and a stylesheet built by repeated escalation ends up with selectors so specific that nothing can change without escalating further.
An element selector spends almost nothing. A class spends a little. A class combined with an element spends a bit more. An ID spends a great deal. The goal is to spend just enough to identify the elements you mean, and no more, leaving headroom above for variations, exceptions and overrides. A card styled with .card can be varied with .card.featured or overridden by a theme rule with a single class; a card styled with body main section div.card can only be varied by something even longer, and the theme author will curse your name.
This principle has a concrete form in the advice to keep selectors short. Short selectors are not just easier to read — though they are — they are also lower in specificity, more resilient to HTML changes, and easier to override. nav a is better than body header nav ul li a on all three counts: it reads clearly, it scores lower, and it survives a restructuring of the navigation markup. Short selectors depend on less, and therefore break less often.
There is a related habit worth building: when a style is not applying, diagnose before escalating. Check whether the selector matches at all, whether the stylesheet is loaded, whether the property is valid — and only then consider specificity. Developer tools, as the previous section explained, make this diagnosis quick: if your rule is absent from the Styles panel, specificity is not the issue; if it is present but crossed out, specificity is the issue, and you can see exactly what beat it and by how much.
Finally, remember that specificity is a means, not an end. The end is a stylesheet that expresses a design clearly, that a team can change confidently, and that survives the addition of new features without rewriting. Low, consistent specificity — mostly single classes, with element selectors for defaults and occasional combinations for genuine precision — produces such stylesheets. High, erratic specificity produces stylesheets that work today and resist change tomorrow. When you write a selector, ask not "will this win?" but "will the next person who needs to change this be able to?" If the answer is yes, the selector is good, however modest it looks. That question, asked consistently, is the difference between CSS that merely works and CSS that lasts.
Check: Why is a very long, very specific selector a problem for future changes?
Section 41 — Naming classes so they make sense
Good class names describe what the thing is, not what it looks like: .warning is better than .red-text, because the warning might one day be styled amber instead of red. Meaningful names survive redesigns. A class named for its appearance — .red-text, .big-box, .left-sidebar — describes a momentary visual fact, and the moment the design changes, the name becomes misleading. A class named for its meaning — .warning, .card, .sidebar — describes a role that persists across redesigns, so the name stays true even when the styling changes completely.
The principle extends beyond colour. Names based on position are similarly fragile: .left-sidebar breaks when the sidebar moves to the right on a new layout, and .top-banner breaks when the banner moves. The test for a good name is to imagine the design changing in a plausible way — a rebrand, a new layout, a colour change — and ask whether the name would still make sense. If it would, the name describes meaning. If it would not, the name describes appearance, and it will eventually mislead someone.
Good names also tend to be short, lowercase, and hyphenated: .site-header, .nav-menu, .card-title. This convention — lowercase words joined by hyphens — is the most widely used in CSS, and following it makes your code look familiar to other developers, which matters more than it might seem: familiar code is read faster and trusted more. Avoid names that are too specific (.homepage-hero-banner-title-text), because they cannot be reused and they encode structure that may change. Aim for the middle: a name that says what the thing is, at the level of the component it belongs to, such as .card, .card-title and .card-body for the parts of a card.
Naming systems formalise these instincts. One well-known approach names classes as block, element and modifier — a card block, its title element, and its featured modifier become .card, .card__title and .card--featured. You do not need to adopt a formal system for a small project, but understanding the idea behind it is valuable: names should reflect the structure of the design system, not the accident of the current styling. When every class name answers the question "what is this thing, and what variation or state is it in?", the stylesheet reads like a description of the design, and new developers can find their way around it without a guide.
Finally, remember that class names are also an interface for other tools. JavaScript selects elements by class, testing tools target classes, and automated tools may rely on class names remaining stable. Renaming a class is therefore not a cosmetic act; it can break scripts and tests, which is another reason to choose names that will last. Spend a moment on each name when you write it, preferring meaning over appearance, and your future self — and your teammates — will thank you every time the design changes and the names still make sense.
Check: Why is .warning a better class name than .red-text?
Section 42 — The danger of styling everything by element
Styling bare elements like div or span affects every one of them on the page, including ones added later by other code. Classes let you opt elements into a style deliberately, instead of catching everything by accident. But for component styles, element selectors are a trap, because they catch everything of that type regardless of context, including elements that belong to other components, third-party widgets, or future features that have not been written yet.
The div is the clearest example. Modern HTML uses div as a generic container for almost everything structural: layout regions, cards, wrappers, widgets, and countless framework-generated elements. A rule like div { padding: 16px; border: 1px solid #ccc; } would give every one of those containers padding and a border, including layout wrappers that should be invisible, nested divs that would accumulate borders, and third-party components whose own styling would be disrupted.
The problem is not just breadth but unpredictability over time. A stylesheet written today cannot know what elements will be added tomorrow: a new widget, an embedded form, a cookie notice, a chat panel. If the stylesheet styles elements by type, those future elements will be caught by existing rules in ways their authors did not anticipate, and the resulting bugs will be attributed to the new code rather than to the over-broad rule that caused them. Classes avoid this by requiring an explicit opt-in: an element receives a component's styles only if it carries the component's class, so new elements are unaffected unless deliberately marked.
There is a middle ground worth noting: scoping element selectors inside a component class. A rule like .article p { line-height: 1.7; } styles paragraphs, but only inside articles, so it does not leak into sidebars or footers. This scoped element styling is a legitimate and common pattern, because within a component, the element type is meaningful: paragraphs in an article really do form a coherent group that should share typography. The danger is unscoped element styling, where the rule reaches across the whole page. The discipline is to ask, for each element rule: is this decision truly universal, or is it really a decision about a particular context? If it is universal — body text line height, link colour, image max-width — an unscoped element rule is correct. If it is contextual — paragraphs in an article, links in navigation, buttons in a form — scope it to the context with a class or a descendant selector.
When you read a stylesheet and find an unscoped rule for a generic element, check which layer it belongs to: if it is a genuine default, it belongs in the base layer and should be minimal; if it is a component style in disguise, it should be rewritten with a class. That check, applied consistently, prevents the slow accumulation of over-broad rules that makes large stylesheets fragile.
Check: What goes wrong when you style div directly on a large page?
Section 43 — Keeping selectors short
Short selectors are easier to read, easier to override, and less likely to break when the HTML changes. nav a is usually better than body header nav ul li a, because the long version breaks if any part of that structure changes. A short selector has low specificity and is easy to override; a long selector has high specificity and resists change. A short selector depends on little structure and survives restructuring; a long selector depends on much structure and breaks when any part of it changes.
Consider body header nav ul li a: it correctly identifies navigation links, but only as long as the navigation is inside a header, inside the body, structured as an unordered list, with links directly inside list items. Wrap the list in a div for layout purposes, or change the list to a different container, or move the navigation outside the header, and the selector stops matching, silently. The style disappears, with no error, and the author must debug a selector that looks correct but no longer describes the actual HTML. The shorter nav a survives all of those changes, because it depends only on the navigation element itself, which is the semantically meaningful container.
Long selectors also mislead about what matters. The meaningful parts are nav and a — the context and the target — and the rest is noise that obscures the intent while adding specificity and fragility. Removing the noise clarifies both the rule and the design: the rule says "links in navigation", and that is exactly what it means.
There are cases where a longer selector is justified. When two similar structures exist on a page — for example, a main navigation and a footer navigation, both using nav elements — a longer selector or a class is needed to distinguish them, and .main-nav a versus .footer-nav a is clearer than any chain of elements. When a style genuinely depends on a structural relationship, such as a direct child or an adjacent sibling, the combinator is meaningful and should be kept. The principle is not "always use the shortest possible selector", but "use the shortest selector that correctly expresses the intent, and prefer a class when the intent is about identity rather than structure". A class like .main-nav is both short and precise, because it names the thing directly instead of describing its ancestry.
As a practical check, look at any selector with more than three parts and ask whether each part earns its place. Does it distinguish this element from a similar one? Does it express a structural relationship that matters? Or is it there "just in case"? Parts that fail that test should be removed. The resulting selectors will be shorter, clearer, lower in specificity, and more resilient — four improvements from a single habit, which is a good return on a small discipline.
Check: Why is nav a more resilient than body header nav ul li a?
Section 44 — Grouping versus repeating
Grouping selectors with commas keeps a stylesheet shorter, but only group selectors that genuinely share a style. If two elements share a colour today but may diverge tomorrow, separate rules are easier to change independently. The misuse is when the sharing is coincidental: two unrelated elements that happen to look alike today, grouped together because it saves a line, and then entangled when one of them needs to change.
The cost of a false grouping becomes apparent at the moment of divergence. Suppose a heading and a button are grouped because both are navy: h2, button { color: navy; }. Months later, the brand colour changes, and buttons should become teal while headings stay navy. The grouped rule must now be split, and the split requires finding the group, understanding that it conflates two decisions, and separating them without breaking anything else. If the rules had been separate from the start, the change would be a one-line edit to the button rule.
The test for whether to group is whether the shared style expresses a shared decision. Headings sharing a font family is a shared decision: the design says "headings use this font", and that decision applies to all heading levels together. If the heading font changes, it changes for all of them, so grouping them expresses the design truthfully. A heading and a button sharing a colour, by contrast, is usually two separate decisions that happen to coincide: the heading colour decision and the button colour decision are made for different reasons, by different considerations, and they can change independently. Grouping them pretends they are one decision, and the pretence will eventually be exposed by a change that affects one but not the other.
In a large stylesheet maintained by a team, the cost grows, because groups are harder to find, the reasons for grouping are forgotten, and splitting a group risks breaking a style that someone else depends on.
A useful alternative to grouping, when styles are shared but the sharing is not a design decision, is to accept a little repetition. Repeating a declaration in two separate rules is not a failure; it is an honest statement that two elements independently have the same value, and it keeps each rule free to change on its own. Modern CSS also offers custom properties — variables — as a way to share a value without sharing a rule: define a colour once as a variable, and use it in separate rules, so that changing the variable updates both, while each rule remains independent. That technique, covered in later chapters, resolves the tension elegantly: shared values, independent rules. Until then, the guideline stands: group what shares a decision, separate what merely shares a value, and prefer a little honest repetition to a misleading grouping.
Check: When should you not group two selectors together?
Section 45 — Order your stylesheet with intent
Put general rules first and more specific overrides later, so source order works with you instead of against you. A common pattern is: defaults, then layout, then components, then small overrides at the end. Stylesheet order is not merely cosmetic; it is part of the cascade, because source order breaks ties between equally specific rules. A stylesheet ordered with intent therefore has fewer surprises: when two rules tie, the later one is the one the author intended to win, because the file is organised from general to specific, and the specific rules naturally come later.
A practical ordering that works for many projects has five layers. First, resets and base styles: the universal box-sizing rule, body typography, link defaults, image defaults — the foundation that everything else builds on. Second, layout styles: the major regions of the page, the header, the main content area, the sidebar, the footer, and the grid or flex containers that arrange them. Third, component styles: the reusable pieces — cards, buttons, forms, navigation, alerts — each as a self-contained block of rules. Fourth, state styles: hover, focus, active, disabled and other pseudo-class rules, which refine components based on interaction. Fifth, utilities and overrides: small, single-purpose classes and page-specific adjustments that are meant to win ties, placed last so that they do.
Within each layer, the same logic applies on a smaller scale: general rules before their variations. A card's base rule comes before its featured variation, a button's base rule before its primary variation, a form's input rule before its error state. When variations are written as modifiers that appear after their base, source order reinforces the intended priority, and the file reads as a story: here is the thing, and here are its variations, in order of increasing specificity of intent.
Ordering also interacts with how stylesheets are loaded. When multiple stylesheets are linked, they form one long stylesheet in link order, so the same general-to-specific logic should govern the link order: a reset or library stylesheet first, the site's base styles next, component styles after that, and theme or override styles last. A common bug is linking a custom stylesheet before a library stylesheet, so that the library's equally specific rules win ties and the customisations appear to fail.
Finally, ordering with intent is a habit that compounds. A stylesheet that starts organised tends to stay organised, because new rules have an obvious place to go: a new component goes in the component layer, a new override goes at the end. A stylesheet that starts disorganised tends to stay disorganised, because new rules are appended wherever is convenient, and the file gradually becomes a chronological record of edits rather than a logical description of the design.
Check: Why put general rules before specific overrides in a stylesheet?
Section 46 — The cascade is a feature, not a bug
The cascade's name sounds like a problem, but it is what lets a small stylesheet style a whole site: set a base font on body, let it inherit everywhere, and override it only where something should differ. Fighting the cascade with !important throws that leverage away. But the surprise is a symptom of unfamiliarity, not a flaw in the design. The cascade is, in fact, the reason CSS can style large sites with small stylesheets, and understanding it as a feature changes how you write CSS entirely.
The leverage comes from combining the cascade's mechanisms. Inheritance spreads text decisions from a few high-level rules to the whole page. The cascade's origin step guarantees that your rules beat browser defaults without a fight. Specificity lets precise rules override general ones predictably. Source order lets later rules refine earlier ones. Together, these mechanisms mean that a design can be expressed as a small set of general decisions plus a small set of exceptions, rather than as an exhaustive list of styles for every element. A site with a thousand pages can be styled by a stylesheet of a few hundred rules, because each rule describes a pattern — a type, a class, a context — and the cascade applies those patterns wherever they match, resolving overlaps by a procedure you can predict.
The cascade is not an obstacle to styling; it is the mechanism that makes styling scalable, and inline styles feel inadequate precisely because they lack it.
Working with the cascade, rather than against it, means designing in layers. Start with the most general decisions: the page's font, colour and line height on body, the default link style, the base spacing. Then add component decisions: cards, buttons, navigation, each as a class with its own styles. Then add variations and states: featured cards, hovered buttons, disabled inputs. At each layer, the rules are a little more specific than the last, and the cascade ensures that each layer refines the previous one without undoing it. That layered thinking is the practical form of "working with the cascade".
It also means accepting that the cascade's decisions are sometimes not what you first wanted, and treating those moments as information rather than frustration. A style that loses a cascade contest is telling you something about your stylesheet's structure: perhaps a rule is more general than you thought, or a variation is placed before its base, or a component is being styled by two different layers that disagree. Listening to that information — reading the Styles panel, scoring the selectors, checking the order — leads to a fix that improves the stylesheet's structure, while forcing a win with !important merely silences the information and leaves the structural problem for the next change.
Check: How does the cascade let a small stylesheet style a whole site?
Section 47 — Case and spelling in selectors
Class and ID names are case-sensitive in HTML and CSS, so .Warning and .warning are different classes. Element names are not case-sensitive in HTML, but writing them lowercase is the convention. A single typo means the rule silently matches nothing.
Case sensitivity is the most common silent failure. In HTML, the class and id attribute values are case-sensitive, and CSS selectors that refer to them must match exactly, including case. An element marked class="Warning" is not matched by .warning, and an element marked id="Main" is not matched by #main. This is easy to get wrong when names are typed by hand in two places — once in HTML, once in CSS — and the two typings differ by a single capital letter. Automated checks, such as searching a project for a class name to verify it appears in both files identically, also catch these errors quickly.
Spelling errors are the same class of problem. A class named .navigaton in the HTML and .navigation in the CSS will never match, and the missing style will look like a cascade problem or a loading problem until someone compares the two spellings character by character. Developer tools help here too: searching the Elements panel for a class name shows whether it exists in the HTML at all, and searching the stylesheet shows whether the rule exists and how it is spelled.
Element names are a partial exception. In HTML, element names are case-insensitive, so <DIV> and <div> are the same element, and a selector written as DIV will match it. But writing element names in uppercase, or mixing cases, is unconventional and confusing, and in XML-based documents such as SVG embedded in HTML, case does matter for element names. The convention of writing all element names lowercase in both HTML and CSS avoids these complications entirely, and it matches the style of virtually all real-world code, which makes your code easier for others to read. Similarly, CSS property names and values are generally case-insensitive, but lowercase is the convention, and following it consistently removes another source of variation.
The broader lesson is that CSS rewards precision and punishes carelessness silently, so precision must become a habit rather than an effort. Type class names carefully, copy them rather than retyping them where possible, use a consistent convention, and verify with developer tools when a style fails. These habits feel pedantic at first, but they prevent a class of bugs that otherwise consumes disproportionate debugging time — not because the bugs are hard, but because they are invisible. A stylesheet is a contract between names in HTML and names in CSS, and like any contract, it works only when both sides spell the names the same way.
Check: Are .Warning and .warning the same class?
Section 48 — Comments in CSS
CSS comments use /* and */ and can span several lines. They are a good place to explain why a tricky rule exists, like /* Overrides the theme's link colour for the footer only */, so future you understands the intent. A rule shows what was done, but it rarely shows why, and the why is what a future maintainer — often yourself, months later — needs in order to change the rule safely. A comment that explains the reason behind a rule preserves the reasoning that would otherwise be lost when the author moves on.
The syntax is simple: everything between /* and */ is ignored by the browser, including line breaks, so a comment can be a single line or a whole paragraph. Always check that every comment is closed, especially after editing one.
Good comments explain why, not what. A comment that says /* Sets the colour to red */ above color: red; adds nothing, because the code already says that, and it will become stale the moment the colour changes without the comment being updated. A comment that says /* Red to match the brand's error colour, not the link colour */ explains a decision that the code cannot express, and it remains useful even if the exact shade changes. Similarly, a comment above a specificity workaround should explain the workaround: /* More specific than the library's rule so the theme wins without !important */ tells the next developer why the selector is long, and warns them not to "simplify" it without understanding the conflict it resolves. Comments are most valuable exactly where the code is most surprising, because surprise is a sign that reasoning has been compressed into a form that needs unpacking.
Comments are also useful for organising a stylesheet. Section headers — /* ===== Header ===== */ or /* Cards */ — divide a long file into navigable regions, making it easier to find rules and to know where new rules belong. This organisational use complements the ordering discipline from Section 45: the file's structure is made visible, so that a developer adding a new component rule can see where the component layer is and place the rule there.
A final caution: comments are shipped to visitors in an unminified stylesheet, so they should not contain sensitive information, internal jokes that would embarrass the team, or disparaging remarks about clients or colleagues. Minification tools strip comments for production, but during development they are visible to anyone who views the source. Write comments as if a client might read them — because, in the source view, a client can. With that caution observed, comments are a low-cost, high-value habit: a few sentences explaining the why behind tricky rules, and clear section headers throughout, turn a stylesheet from a list of instructions into a document that explains its own design.
Check: How do you write a comment in CSS?
Section 49 — Debugging a style that will not apply
When a style seems ignored, check four things in order: is the stylesheet linked, is the selector spelled exactly right, is a more specific rule overriding it, and is the property valid? Most "CSS is broken" moments are one of these four. The four checks in this section cover the overwhelming majority of styling failures, and working through them in order saves the time that random experimentation wastes.
The first check is whether the stylesheet is loaded at all. Open the page, open developer tools, and look for the stylesheet in the Network panel or in the Styles panel for an element: if none of the stylesheet's rules appear, the link is broken, the path is wrong, the file was not saved, or the browser is showing a cached version. A hard refresh, which bypasses the cache, eliminates the caching explanation. This check sounds too basic to mention, but it is the cause of a surprising number of "CSS stopped working" reports, precisely because it is too basic to suspect.
The second check is whether the selector matches the element. In developer tools, inspect the element and look for your rule in the Styles panel. If it is absent, the selector does not match, and the causes are usually a typo, a case mismatch, a wrong combinator, or a misunderstanding of the HTML structure — the near-misses from Section 38, or the case sensitivity from Section 47. Comparing the selector with the element's actual HTML, attribute by attribute, usually reveals the discrepancy.
The third check is whether a more specific or later rule is overriding yours. If your rule appears in the Styles panel but is crossed out, it matched but lost the cascade, and the winning rule is shown above it. Score the two selectors, check their order, and decide how to respond: usually by making your selector appropriately more specific, or by moving it later, or by recognising that the other rule should win and adjusting your intention instead. Do not reach for !important at this stage; it masks the cascade problem rather than solving it, and it creates a harder problem for the next change, as Section 28 explained.
The fourth check is whether the property and value are valid. A misspelled property — colour instead of color, font-wight instead of font-weight — is silently ignored by the browser, as is a value the property does not accept, such as display: centre or a colour written incorrectly. Developer tools show invalid declarations crossed out or with a warning icon, depending on the browser, which makes this check quick once you know to look. Also check that the property applies to this kind of element: some properties have no effect on certain display types, and a valid rule can still appear to fail if the property is inapplicable.
Check: Name the four things to check when a style does not apply.
Section 50 — Chapter 6 in one paragraph
CSS works by selectors picking elements, declarations styling them, and the cascade deciding conflicts through origin, specificity and source order, with inheritance filling in the gaps. Once you can predict which rule wins, you can read any stylesheet and fix styling bugs instead of guessing. Selectors are the addressing system: they describe patterns of elements, from the broadest — every element, every paragraph — to the most precise — a particular element in a particular state in a particular context. Declarations are the styling itself: properties and values that describe how a matched element should look. And the cascade, with inheritance as its fallback, is the decision procedure that resolves the inevitable overlaps when several patterns match the same element and disagree.
The journey through the chapter followed a deliberate order. It moved to combinators — descendant, child, sibling — because real designs are about relationships and context, not just isolated elements. Then it turned to the cascade, because once you can write many selectors, you need to know what happens when they overlap, and the answer — origin, then specificity, then source order, with inheritance filling the gaps — is a procedure you can apply mechanically to any conflict. The later sections were about judgement: naming, brevity, ordering, debugging, and the discipline of keeping specificity low, because writing CSS that works is only half the job, and writing CSS that lasts is the other half.
If you take one practical skill from this chapter, let it be the ability to predict a cascade outcome before you test it. Look at an element, list the rules that target it, score their selectors, check their order, and name the winner — and then verify with developer tools. When your prediction matches the browser's answer consistently, you have understood the cascade, and CSS will feel like a system you control rather than a mystery you endure. And if you take one attitude, let it be to treat the cascade as a feature: a small set of general rules, refined by a small set of specific ones, can style a whole site, and that leverage is the reason CSS is worth learning deeply rather than merely copying from examples.
The next chapters build directly on this foundation. Chapter 7 explores the values you put into declarations — colours, fonts, text properties — in the depth they deserve, because a selector is only as useful as the style it applies. Chapter 8 explores the box model, which is where selectors meet layout: how every element is a box, and how those boxes are sized and spaced. Both chapters assume the selector and cascade knowledge from this chapter, and both reward it: when you understand which rule wins, you can focus on what the winning rule should say, which is where design actually happens.
Check: In your own words, what three things decide which CSS rule wins a conflict?
Practical task
Build a small page with a heading, three paragraphs and a list, and a stylesheet that uses at least one element selector, one class selector, one ID selector, one descendant selector and one hover style. Then deliberately create a conflict — set the same property with two selectors of different specificity — open developer tools, find the crossed-out declaration, and write a comment in your CSS explaining which rule won and why.
Chapter questions
- Explain specificity in plain words, using an example with a class and an ID.
- Write a selector that targets only list items that are direct children of a navigation list.
- Two rules set the same property on the same element: one is more specific but appears earlier, the other is less specific but appears later. Which wins, and why?
- Why does setting a font on
bodystyle the whole page, while setting a margin onbodydoes not have the same spreading effect? - A style you wrote is not showing up. List the steps you would take to find out why.