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.