CSE 134B Web Client Languages
  1. Presentation
  2. CSS
  3. Cascade
  4. The Cascade, Inheritance and Scope

Presentation · CSS 03

The Cascade, Inheritance and Scope

Many rules can set the same property on the same element. The cascade is the algorithm that picks one winner, and inheritance fills in every property that no rule set at all. Knowing both turns CSS from a guessing game into a language you can reason about.

This page treats the cascade as what it is: a sort with a fixed list of tie-breakers. It then covers the four tools an author has for controlling that sort in modern CSS (@layer, @scope, nesting, and the restraint to leave !important alone), follows a value from the style sheet to the screen, and ends by applying all of it to a familiar mess: markup infected with presentational classes.

1. The cascade as a sort

Pick any element and any property, say the color of one link. The browser's own style sheet sets it. A reset may set it. A base rule for links sets it, a component rule sets it, a utility sets it, and a style attribute might set it too. The CSS specifications call every one of those a declared value. The element can only have one color, so the browser needs a rule for choosing.

"The last one written wins" is true, but only at the very end. The cascade is a multi-stage sort. Each stage compares the competing declarations on one criterion. If one declaration comes out ahead, the sort stops and that declaration wins. If several tie, only the tied ones move on to the next stage. The winner is the cascaded value.

The cascade as a funnel of tie-breakersEvery declaration of one property on one element enters at the top. Seven stages narrow them down in order: 1, origin and importance; 2, context, meaning shadow trees; 3, element-attached styles from the style attribute; 4, cascade layers; 5, specificity; 6, scope proximity; 7, order of appearance. Each stage runs only when the stages above it tied. The first three stages depend on where the CSS comes from; the last four depend on how the author writes it. One winner, the cascaded value, comes out at the bottom.every declaration of one property on one element1 · Origin and importancebrowser, user, author; !important reverses them2 · Contexta shadow tree against the page around it3 · Element-attached stylesthe style attribute beats rules with selectors4 · Layersunlayered beats layered; a later layer beats an earlier one5 · Specificity(ids, classes, types), compared left to right6 · Scope proximitythe nearer @scope root wins7 · Order of appearancethe last declaration winsone winner: the cascaded valueeach stage runs only on a tiewhere the CSScomes fromhow youwrite it
The cascade as a funnel. Most conflicts are settled long before specificity is consulted.

Two things about the funnel matter more than the details. First, the order is fixed: a stage lower down can never overturn a decision made higher up. A selector with enormous specificity still loses to a plain one in a later layer, because layers are compared first. Second, the bottom four stages are the ones you control by how you write a style sheet, and they are the subject of most of this page.

Origin and importance

CSS Basics introduced the three origins (user-agent, user and author) and the way !important reverses their order. The full first stage has two more entries. While a CSS animation runs, its values sit just below every important declaration and above all normal ones. While a transition runs, its values beat everything, because a transition is meant to move smoothly from the old value to the new one whatever else is going on.

Folding in the later stages that matter most to authors, the style attribute and layers, gives the complete order. It reads strongest first:

RankDeclarationsNotes
1Transitions in progressOnly while a transition is running
2User-agent !importantThe browser protecting its own constraints
3User !importantThe reader's settings and accessibility tools
4Author !important in a style attributeElement-attached beats rule-based
5Author !important in the first layer declaredImportance reverses the layer order …
6Author !important in later layers, in turn… so earlier layers win here
7Author !important, unlayeredThe weakest important author styles
8Animations in progressOnly while a @keyframes animation is running
9Author normal, in a style attributeBeats every author rule in a style sheet
10Author normal, unlayeredThe strongest normal style sheet rules
11Author normal, in the last layer declaredLater layers win …
12Author normal, in earlier layers, in turn… down to the first layer
13User normalThe reader's defaults, such as a preferred font size
14User-agent normalThe browser's style sheet

Within any one row, specificity, scope proximity and order of appearance break the remaining ties. Notice where the style sheet you write spends its life: almost everything is in rows 10 to 12. Those rows tie at the first stage, and that is why layers, specificity and order do the real work.

The stages after origin

  • Context separates a web component's shadow tree from the page around it. For normal declarations the outer page wins, so a page can restyle a component it uses; for important ones the component's own styles win, so it can protect what it must.
  • Element-attached styles are the declarations in a style attribute. They have no selector, so they skip specificity entirely and beat every rule of the same origin and importance. "Inline styles have the highest specificity" is a common shorthand for this, but the real mechanism is a stage of its own.
  • Layers, specificity and scope proximity each get a section below or on the selectors page.
  • Order of appearance is the final word: when everything else ties, the declaration that comes last wins, counting through linked sheets, @imports and style elements in the order the document includes them.

2. !important, and why layers replace it

Older CSS teaching, this course's earlier slides included, offered !important as the way to say "this rule should never be overridden":

p { color: red !important; font-size: 12pt; }

The keyword attaches to a single declaration, here only the color, and moves it into a stronger bucket at the first stage of the cascade. That sounds like a clean tool. In practice it starts an arms race. A third-party sheet uses !important to win, so the site's own sheet uses it to win back, so the next fix needs it too. Each use removes an escape route, and specificity battles reappear inside the important bucket. A style sheet full of !important is one in which nobody can predict what wins.

Users always win

The keyword also cannot do what its name promises. As the origins table shows, importance reverses the order of origins: a user's !important beats an author's !important. That is deliberate policy. A reader with low vision who forces larger text or higher contrast, through browser settings, an extension such as Stylus, or a forced-colors mode, must be able to override any page. Try it with any user style extension: change a site's fonts, hide its banners, enlarge its text. No author declaration, important or not, can stop it. The web is a medium where the reader has a say, and !important was built partly to guarantee that.

What authors were really asking for

Most author uses of !important were never about one declaration. They were about a group of rules: "the site's own styles should beat the framework's", or "utilities should beat components". Specificity cannot say that, and source order is fragile once files load from several places. @layer says it directly. A rule in a later layer beats any rule in an earlier one, however specific, with no keyword at all:

@layer reset, base, components, utilities; @layer components { main article nav a { color: var(--link); } } @layer utilities { :where([data-tone="calm"]) { color: var(--calm); } }

The utility's selector has a specificity of zero and the component's has four type selectors, yet the utility wins, because utilities was declared after components. The house style in this section never uses !important. A short list of deliberate guards remains defensible, such as [hidden] { display: none !important; } to keep the hidden attribute working when other rules set display, but those are rare, commented decisions rather than a way to win arguments.

3. @layer

Cascade layers give a style sheet named tiers inside the author origin. They have been Baseline since 2022 and are now widely available, so they are safe to use everywhere.

Three ways to write a layer

The statement form declares names and their order without any rules. Write it once, at the top of the first style sheet:

@layer reset, tokens, base, layout, components, utilities;

The block form puts rules into a layer. Blocks can appear in any order and in any file; each one joins the layer of its name, and the layer keeps the position the statement gave it. Repeating a block name adds to the same layer:

@layer base { body { font: 1.0625rem/1.6 system-ui, sans-serif; } a { color: var(--accent); } } @layer reset { *, *::before, *::after { box-sizing: border-box; } }

The import form, shown in CSS Basics, places an entire file into a layer: @import url("vendor.css") layer(vendor);. This is the cleanest way to bring in a framework or a third-party widget's CSS without letting it outrank your own.

A few more details are worth knowing. A layer's position is fixed the first time its name appears, which is why the statement belongs at the very top. A name first seen later is added after all the existing layers. A @layer { … } block with no name creates an anonymous layer that nothing else can add to. Layers can nest, as in @layer components { @layer card { … } }, and the inner layer can then be referred to as components.card.

Where layers sit

Layers are compared at stage 4 of the cascade, after origin and importance, after context and after the style attribute, and before specificity. Three rules follow from that position:

  • The first layer declared is the weakest; the last is the strongest. It matches the cascade's general habit that later wins.
  • Unlayered styles beat every layer. Any rule not inside a layer acts as if it were in one final, implicit layer after all the named ones.
  • A style attribute beats every layer as well, because element-attached styles are compared one stage earlier.

The second rule surprises people who expect layers to add priority. It is the safe default: an existing style sheet with no layers keeps winning over any library you move into a layer, so adopting layers can start with a single @import … layer(vendor) and nothing else changes.

Importance reverses the layers

The cascade has one consistent rule for !important: it reverses ordering. It reverses the origins, and it reverses the layers too. For important declarations, an earlier layer beats a later one, and unlayered important declarations are the weakest important declarations of all.

Layer order for normal and for important declarationsTwo columns, strongest at the top. For normal declarations the order is unlayered, utilities, components, layout, base, reset. For important declarations it is reversed: reset, base, layout, components, utilities, unlayered. Lines connect each layer in the left column to the same layer in the right column, crossing in the middle. Unlayered styles are strongest when normal and weakest when important.normal declarations!important declarations(unlayered)utilitiescomponentslayoutbaseresetresetbaselayoutcomponentsutilities(unlayered)strongerstrongerAll important author declarations beat all normal ones; the order inside each group reverses.
One stack, read in two directions: later layers win for normal declarations, earlier layers win for important ones.

The reversal has a practical consequence. Put a framework in a vendor layer and its normal rules lose to all of yours. Its !important declarations, though, still beat your normal ones, and they also beat any !important you write in a later layer or unlayered. To outrank them with importance you would need a layer declared before vendor. Usually the better answer is not to fight at all: report the problem upstream, or replace the rule.

Backing out of a layer

The global keyword revert-layer rolls a property back to whatever the cascade would have produced without the current layer: the value from earlier layers, or from the browser if no layer set one. A component layer can use it to defer to the base styles for one property without repeating them. The properties page covers it alongside the other global keywords.

4. @scope

Sometimes a group of rules should apply only inside one part of the page, and stop at a boundary inside that part. Article typography should not leak into an embedded widget. A card's styles should not reach into a card nested inside it. Before @scope, the choices were long descendant selectors, naming conventions such as BEM, or build tools that rewrote class names. Each one costs something: brittle selectors, verbose markup, or a dependency on the build.

@scope is the language's own answer. It shipped in Chrome in 2023 and Safari in 2024, and reached Baseline (newly available) once Firefox, the last of the major engines, added it. For anything a page cannot live without, treat it as an enhancement for now.

The scope root, and the limit

The prelude names a scope root. Rules inside match only the root and its descendants:

@scope (main > article) to (figure, aside) { :scope { max-inline-size: var(--measure); } p { line-height: 1.6; } a { color: var(--accent); } }

The optional to (…) clause names a scope limit, a lower boundary. Elements matching the limit, and everything inside them, are outside the scope. The styled region is shaped like a ring with a hole in it, which is why this pattern is often called donut scope. Inside the block, :scope matches the root itself.

Donut scope: a root, and a limit that cuts a holeA document tree. An article element is the scope root. Its children are an h2, a p and a figure. The p contains an a. The figure contains an img and a figcaption. The article, h2, p and a are inside the scope and can be matched by the scoped rules. The figure is the scope limit, so it and its img and figcaption are outside the scope.articlescope rooth2pfigurescope limitaimgfigcaptionin scope: the scoped rules can match itoutside the scope
@scope (article) to (figure): the scoped rules reach the article and its content, but not the figure or anything inside it.

A style element can also use @scope with no prelude at all. The scope root is then the style element's parent, which gives HTML a working form of the long-abandoned <style scoped> idea:

<section> <style> @scope { img { border-radius: 0.5rem; } } </style> <img src="trail.jpg" alt="The Cowles Mountain trail at dawn"> </section>

Scope adds structure, not specificity

The root selector in the prelude does not count toward the specificity of the rules inside. A bare p inside @scope (main > article) matches like main > article p but keeps the specificity of a single type selector, (0, 0, 1). Writing :scope p adds a pseudo-class and gives (0, 1, 1). That is the point of the feature: it constrains where rules match without making them harder to override.

Proximity

@scope also added a stage to the cascade. When two scoped rules of equal specificity disagree, the one whose scope root is nearer to the element, counting steps up the tree, wins. Without that stage, nested themes would fail: in a light section placed inside a dark page, both themes' link rules would tie on specificity, and source order would pick the same theme everywhere regardless of nesting.

@scope ([data-theme="dark"]) { a { color: var(--link-on-dark); } } @scope ([data-theme="light"]) { a { color: var(--link-on-light); } }

A link inside a light section, inside a dark page, takes the light color, because the light root is the nearer ancestor. Flip the nesting and the dark color wins, whatever order the rules are written in.

What scope does not isolate

@scope limits which elements its selectors can match. It does not stop inheritance. If a scoped rule sets color on a p, every element inside that p inherits the color, including elements inside a scope limit. Stopping an inherited value at a boundary takes an explicit declaration on the boundary element, such as color: initial or a fresh value. Full style isolation, where nothing outside gets in, is what the shadow DOM of web components provides.

5. Nesting

CSS Basics introduced nesting as a way to make a style sheet follow the shape of its HTML. Native nesting has been Baseline since 2023 and is now widely available, so the feature that once required Sass or Less now works without a build step.

Nesting is notation

A nested rule produces exactly the selectors a flat style sheet would have written out. Nothing about which elements match changes; only the text is organized differently. This rule:

article { padding: var(--space); > h2 { margin-block-start: 0; } & + & { border-block-start: 1px solid var(--rule); } a:where(:hover, :focus-visible) { text-decoration-thickness: 0.15em; } aside & { font-size: 0.9em; } @container (inline-size > 40rem) { padding: calc(var(--space) * 2); } }

means the same as these flat rules:

article { padding: var(--space); } article > h2 { margin-block-start: 0; } article + article { border-block-start: 1px solid var(--rule); } article a:where(:hover, :focus-visible) { text-decoration-thickness: 0.15em; } aside article { font-size: 0.9em; } @container (inline-size > 40rem) { article { padding: calc(var(--space) * 2); } }

The rules for & are short:

  • & stands for the parent selector, and can appear anywhere in the nested selector: &:hover, & + &, aside &.
  • With no &, a nested selector is a descendant of the parent: a inside article means article a, and > h2 means article > h2.
  • A pseudo-class attached to the parent needs the &: &:hover is the article being hovered, while :hover on its own means any hovered descendant.
  • Conditional at-rules such as @media, @container and @supports nest too, and declarations directly inside them apply to the parent selector. Keeping a component's adaptations next to its base styles is one of nesting's best uses.

Not Sass

Native nesting joins selectors, not strings. Sass lets &__title glue a suffix onto a class name, a habit of BEM-style CSS:

.card { &__title { font-size: 20px; } }

In the browser, &__title is not a valid selector, so the rule is dropped. The structure-first answer does not need the trick: the title of a card is its heading, and article { > h2 { … } } selects it.

The specificity of &

& carries the specificity of its parent as if the parent were wrapped in :is(). When the parent is a list, that means the highest specificity in the list, applied to every match:

article, .featured, #promo { &:hover { outline: 2px solid; } }

Here even a plain article on hover is matched at (1, 1, 0), the id's specificity plus the pseudo-class, and anything later that tries to override it must outweigh an id. Wrapping the list in :where() sets the parent's contribution to zero, so the nested rule counts only its own selector:

:where(article, aside) { &:hover { outline: 1px solid var(--rule); } }

How deep to nest

Nest when the inner rule cannot meaningfully exist without the outer one: states such as &:hover and &[aria-expanded="true"], direct parts such as > h2, and media or container adaptations. Stop at two levels. By the third, a reader has to unroll the selector in their head to know what it matches, and its specificity and its coupling to the markup have grown just as if it were written out longhand. If a nested rule could stand alone, such as buttons that look the same everywhere, it belongs at the top level.

6. Inheritance, and the computed-value pipeline

The cascade picks a winner when declarations compete. Most of the time, for most properties, nothing competes at all: no rule anywhere sets the color of a particular em. The value comes from inheritance, and this is where the document tree from the selectors page pays off. A property set on an ancestor flows down to its descendants unless something stops it.

Which properties inherit

Each property's definition says whether it inherits. The pattern follows common sense about documents:

Kind of propertyInheritsExamples
Text and typographyYescolor, font-family, font-size, line-height, text-align, letter-spacing
ListsYeslist-style-type, list-style-position
Visibility and directionYesvisibility, direction, writing-mode, cursor
Custom propertiesYes, by default--accent, --space
Box model and layoutNomargin, padding, border, inline-size, display, position
BackgroundsNobackground-color, background-image

Typography is something you set on a container and expect everything inside to share; set the font once on body and the whole page follows. Boxes are per element: if a section's padding were inherited, every paragraph inside it would be padded again. Custom properties inherit so that a token set once on :root is visible everywhere. A typed custom property registered with @property can opt out with inherits: false, as the values page shows.

When a property has no cascaded value, the browser fills one in, a step the specification calls defaulting. An inherited property takes the parent's value. A non-inherited property takes its initial value, the default written into its definition, such as transparent for background-color. Any property can be pushed the other way with the global keywords: inherit forces inheritance even for a box property, and initial blocks it. A common use is button { font: inherit; }, which lets form controls share the page's typography instead of the browser's defaults.

The computed-value pipeline

The cascade's winner is not yet a number the screen can use. 1.25em depends on the parent's font size; 50% depends on the size of the container; var(--space) is only a reference. Each property on each element passes through a pipeline of values, each one resolved a little further than the last.

From declared value to actual valueSix stages in a row, joined by arrows: declared, cascaded, specified, computed, used and actual. Three examples run beneath them. Font-size on an h2 in a 16 pixel article: the declared values are 1.5em from the browser and 1.25em from the author; the cascaded and specified value is 1.25em; the computed, used and actual value is 20px. Inline-size of 50 percent in a 723 pixel container: the value stays 50 percent until the used stage, where it becomes 361.5px, and the actual value may be rounded to about 362px. Color on a span that no rule matches: there is no declared or cascaded value, so the specified value is inherited from the parent, #1f2430, and it stays the same through the remaining stages.declaredcascadedspecifiedcomputedusedactualevery matchingdeclarationthe cascade'swinner, if anyor else inheritedor initialvar() and emresolvedafter layout:%, auto resolvedfitted to thedevicefont-size on an h2, in an article whose font-size is 16px1.5em (UA)1.25em1.25em1.25em20px20px20pxinline-size: 50%, inside a container 723px wide50%50%50%50%361.5px≈ 362pxcolor on a span that no rule matches, inside a p whose color is #1f2430(none)(none)#1f2430(inherited)#1f2430#1f2430#1f2430
Every property on every element passes through the same six stages. The cascade only decides the first two.
ValueWhat it is
DeclaredEvery declaration, from every origin, that matches the element and sets the property. There may be none or dozens.
CascadedThe winner of the cascade. If no declaration applies, there is no cascaded value.
SpecifiedThe cascaded value, or, when there is none, the inherited or initial value from defaulting. Every property on every element has one.
ComputedThe specified value with what can be known before layout resolved: var() substituted, em and rem turned into absolute lengths, keywords such as bolder resolved. This is the value that children inherit.
UsedThe computed value after layout: percentages of a container's size and auto become real lengths.
ActualThe used value adjusted to what the device can do, such as rounding to whole device pixels or using the nearest available font size.

The pipeline explains a few behaviors that otherwise look arbitrary. DevTools' Computed panel shows the computed value, or for some properties the used value, which is why it can report pixels for a size you wrote in rem. getComputedStyle() in JavaScript returns the same thing, for the same reason.

Children inherit the computed value

Inheritance passes down the parent's computed value, not the text that was written. That matters most for line-height:

article { font-size: 1rem; line-height: 1.5em; } aside { font-size: 1rem; line-height: 1.5; } :is(article, aside) h2 { font-size: 2rem; }

In the article, 1.5em computes to an absolute length, 24 pixels at the default font size, and that length is what its h2 inherits: a 32-pixel heading squeezed into 24-pixel lines. In the aside, the unitless 1.5 computes to the number itself, so each descendant multiplies its own font size by 1.5 and the heading gets 48-pixel lines. Write line-height as a plain number. The same reasoning explains currentColor, which stays live: it always means the element's own color, whatever that turns out to be.

7. Applied: from class-itis to structure-first styling

The cascade, layers, scope and inheritance all point toward the same way of writing CSS. The clearest way to see it is to start with the opposite.

Presentational class-itis

The markup below starts out well, with a real h1. Then it catches what this course calls presentational class-itis: class names that describe a look rather than a meaning.

<h1 class="xl-size underline style-8">My Important Heading</h1> .xl-size { font-size: 48px; } .underline { text-decoration: underline; } .style-8 { color: #c00; font-family: Georgia, serif; margin-left: 12px; }

The result looks reasonable, and that is the trap. The presentation has moved back into the HTML; the separation of concerns that CSS Basics started with is gone. Redesigning the headings now means editing markup on every page that uses them. The name underline becomes a lie the day the design drops underlines. style-8 says nothing to the next developer, and the fixed pixel sizes and the physical margin ignore the reader's font settings and the writing direction. It is <font> tags again, with extra steps.

Three better bindings

The course's original slides offer three alternatives, and each is better than class-itis:

  1. Style the element itself when every h1 should look this way: h1 { … }.
  2. Use a meaningful class when only some h1 elements should: <h1 class="intro-heading">. The name says what the heading is, not how it looks.
  3. Rely on context: the heading of the page's introduction is already identifiable by where it sits in the structure.

The slides then add the crucial caveat: whichever selector you choose, values such as 48px and #c00 are magic numbers mixed into the rules. Treat them the way you would in any other code: name them once as constants. In CSS those constants are custom properties.

The structure-first version

The markup says only what things are:

<body> <header> <h1>My Important Heading</h1> <p>A short introduction.</p> </header> <main> … </main> </body>

And the style sheet binds the look to that structure, with every value named and every rule in a layer:

@layer reset, tokens, base, layout, components, utilities; @layer tokens { :root { --accent: #a3420f; --font-display: Georgia, serif; --step-4: clamp(2rem, 1.5rem + 2.5vw, 3rem); --space: 1rem; } } @layer base { h1 { font-family: var(--font-display); font-size: var(--step-4); line-height: 1.1; text-wrap: balance; } } @layer layout { body > header { padding-inline: var(--space); > h1 { color: var(--accent); text-decoration: underline 0.06em; text-underline-offset: 0.15em; } } }

Every h1 gets the display font and a fluid size that respects the reader's default text size. The page heading, identified by its place in the structure, adds the accent color and the underline. Changing the design means changing the style sheet, once. If a later design needs a heading variant that the structure cannot express, a meaningful hook is still welcome, written so it adds no weight: h1:where(.intro-heading) keeps the specificity of a bare h1.

An order for the whole style sheet

The slides also proposed an order for a style sheet: variable definitions, a reset, utilities, broad element rules, elements in context, class overrides, id rules, and finally hacks. Before layers, that order could only be kept by discipline and source order. Layers turn it into something the browser enforces:

The slides' orderToday
Variable definitionsA tokens layer of custom properties on :root.
A resetA small reset layer, first and weakest.
UtilitiesA utilities layer, moved to the end, so a utility reliably wins without !important.
Broad elementsA base layer of element selectors.
Elements in contextA layout layer, with nesting and @scope to express the context.
Class overridesA components layer, using classes only where structure cannot say it, wrapped in :where().
Specific id rulesNone. An id is a link target and a label reference, not a style hook.
HacksIdeally none. A quick fix written unlayered will win and stand out, so it can be folded back into the right layer later.

The slides' warning still holds: follow an order like this with sound markup and things work very well; skip it and you may find yourself with a 2,500-line style sheet, fighting for your layout's life. The difference now is that the cascade itself can carry the architecture, instead of every developer's memory.

8. Summary

ConceptKey points
The cascade as a sortStages in a fixed order: origin and importance, context, the style attribute, layers, specificity, scope proximity, order of appearance. Each runs only on a tie; the winner is the cascaded value.
Origins in fullTransitions beat everything; important declarations reverse the origin order; animations sit between important and normal author styles.
!importantReverses origin and layer order and starts arms races. Users always win with it, by design. Use layers to rank groups of rules instead.
@layerDeclare the order once at the top. Later layers beat earlier ones, unlayered beats all layers, and the style attribute beats both. Importance reverses the layer order.
@scopeA root and an optional limit bound where rules match, without adding specificity. Nearer roots win ties. It does not stop inheritance. Baseline newly available.
NestingNotation for selectors, not string concatenation. & carries the parent's specificity as :is() does; use :where() to zero it. Stay within two levels.
InheritanceText properties and custom properties inherit; box and background properties do not. Missing values default to the inherited or the initial value.
Value pipelineDeclared, cascaded, specified, computed, used, actual. Children inherit the computed value, so write line-height as a plain number.
Structure firstReplace presentational class-itis with element and structural selectors, named tokens, and layers that enforce the style sheet's order.