Why Deck uses cascade layers

Deck places every one of its rules inside a cascade layer, so overriding Deck with confidence requires understanding how cascade layers are ordered. Two rules catch people. A layer's position is fixed where its name is first declared, so CSS that loads before Deck loses to Deck. And CSS written outside any layer outranks every layer, however specific the layered selector is. Deck accepts that learning cost because, once the two rules are known, every override of Deck is a one-class selector with no !important.

What the model costs

Load order decides whether an override of Deck works. Deck's stylesheet begins by declaring fifteen cascade layers in a fixed order: eleven for Deck, from deck.reset to deck.print, followed by four for application code, named app.base, app.components, app.pages and app.overrides. On a page that loads Deck first and then writes .btn { border-radius: 0 } inside @layer app.pages, the button's corners measure 0px in Chrome. When the identical override sits in a style block before Deck's stylesheet, the corners measure 7px, which is Deck's value, because that first mention of app.pages created the layer ahead of every Deck layer.

<!-- Deck wins: app.pages is created before Deck's layers exist -->
<style>@layer app.pages { .btn { border-radius: 0; } }</style>
<link rel="stylesheet" href="/assets/deck/deck.min.css">

<!-- The override wins: Deck has already placed app.pages last -->
<link rel="stylesheet" href="/assets/deck/deck.min.css">
<style>@layer app.pages { .btn { border-radius: 0; } }</style>

CSS written outside any cascade layer outranks all layered CSS, regardless of specificity. On a page with Deck loaded, an unlayered rule as weak as button { border-radius: 3px } beats a rule inside @layer app.overrides whose selector is main > button.btn.btn-primary#probe, which contains an ID and three classes, and the button measures 3px in Chrome. A developer who expects specificity to settle that conflict will be wrong, and the only explanation the browser offers is the layer label that developer tools print beside each rule.

Importance reverses cascade layer order: among declarations marked !important, the earlier layer wins. Deck's reset layer sets transition-duration: .01ms !important for readers whose operating system is set to reduce motion. With that preference on, an application rule of transition-duration: 5s !important inside @layer app.overrides loses, and Chrome reports a duration of 0.01ms; with the preference off, the application rule applies and the duration is 5s. The inversion is correct behaviour and it protects the reader's accessibility setting, but it defeats anyone who reaches for !important to force an override of Deck.

The need to explain overriding shows in Deck's own documentation. Of the 87 component, reference, tutorial and guide pages on Deck's documentation site, 76 contain an example written inside an @layer app block, because nearly every page about a component has to show how to change that component safely.

What layers replace

Before cascade layers, a CSS framework and the application using it competed on specificity. An application overriding a framework selector such as .card .card-header .card-title needed a selector at least as specific, then !important when that was not enough, and a framework that also used !important left nothing further to escalate to. Which rule won depended on how selectors happened to be written rather than on which code was meant to have the final say.

Deck's source uses !important 101 times, and the uses are concentrated rather than scattered. 84 are inside @media print, where paper styling must replace screen styling. 7 are inside prefers-reduced-motion, where a reader's accessibility setting must replace animation. Of the remaining 10, six reset positioning, shadows and clipping on table cells when Deck's data grid restacks into cards on a narrow screen, two implement the .no-motion opt-out, and two style an item being dragged and the gap it leaves behind.

Another way for a framework to yield to application CSS is to wrap its selectors in :where(), which reduces their specificity to zero so that any application selector wins. Zero specificity also loses to bare element selectors: in Chrome, a rule of :where(.note) { color: blue } is overridden by span { color: red } on the same element, and the text renders red. Under that approach, an application's ordinary element defaults, such as a style for every button or every link, silently override a framework's component styling.

Cascade layers let Deck keep normal specificity inside itself while yielding to application code as a single block, and they also rank Deck's own parts against each other. In Chrome, an input carrying Deck's .border utility inside an .input-group shows a 1px solid border, even though the rule .input-group > .input removes borders with a selector of two classes against the utility's one. Deck's utilities layer comes after its components layer, so the one-class utility wins, which is the result a developer adding .border intends.

What the order buys

Deck declares the four application layers inside its own stylesheet, after all eleven of its own, so an application never has to create those layers or put them in order. Once Deck's stylesheet has loaded, a one-class selector inside an application layer beats every Deck rule without !important.

@layer app.components {
  .card { border-radius: 0; }
}

Deck's four application layers also order an application's CSS against itself. Rules in app.base, such as element defaults and token overrides, lose to app.components; rules in app.components lose to app.pages; and app.overrides beats all three. An override that must win immediately can go in app.overrides and still be found later by searching for that layer name, which cannot be said of an !important scattered through a codebase.

Cascade layers add no browser-support cost to Deck. They have been Baseline widely available since March 2022, having first shipped in Chrome and Edge 99, Firefox 97 and Safari 15.4 according to caniuse. The light-dark() function that Deck's colour system depends on arrived later in every one of those browsers, so cascade layers never determine Deck's minimum browser versions.

Who this model does not suit

Deck's cascade layer model suits applications that control the order in which their stylesheets load. A site where a plugin, a tag manager or an embedded widget can insert a stylesheet into the page head before Deck's link tag cannot guarantee that Deck loads first, and any layer such a stylesheet names early will be positioned ahead of Deck's layers.

A codebase with a large amount of unlayered legacy CSS will find that the legacy rules beat Deck wherever the two overlap, because CSS outside a cascade layer outranks every layer. Adopting Deck in that codebase means moving the legacy CSS into an application layer first, or accepting that the old rules will keep winning.

A team that settles styling conflicts with !important will find the habit works against Deck. Because importance inverts layer order, an !important declaration in app.overrides loses to an !important declaration in any Deck layer, including the reduced-motion rules in Deck's reset layer.

The case in one paragraph

Deck's argument for cascade layers is that two rules about load order are easier to learn than an unbounded contest of selector specificity. Load Deck's stylesheet before any application CSS and keep that CSS inside the four application layers, and every override of Deck becomes a one-class selector without !important. The rules have to be learned once, and Deck's documentation repeats them on 76 pages because nearly every component page has to show how that component is overridden.

Related: why Deck has no build step, why Deck themes from one hue, and the task-oriented guide to overriding Deck without !important.