Cascade Layers and Overriding Leed

The layer decides who wins — not specificity, and not !important. If your rules are losing to Leed’s, the fix is almost never a longer selector or a bang; it is an annotation on the @import line that brings your file into the right layer.

The layer order Leed declares

leed.css declares the order before any import:

@layer theme, base, components, utilities;

Declaring the order up front is what makes it stable: layers take their precedence from the order they are named, not the order rules happen to arrive in. Whatever imports what, theme loses to base, which loses to components, which loses to utilities.

Two rules of @layer are worth stating explicitly if you have not used it before, because they are the opposite of the intuition specificity trains:

  • A later layer beats an earlier layer at any specificity. A single-class rule in utilities beats a five-class rule in components. Specificity is only consulted within a layer.
  • Unlayered CSS beats every layered rule. Rules outside all layers are compared last and win, again at any specificity.

That second rule is the escape hatch this page returns to twice.

How to override a Leed component style

Three steps:

  1. Put your rules in their own file under tailwind/ — say tailwind/docs/components.css.
  2. Import it from tailwind/site.config.css with an explicit layer: @import "./docs/components.css" layer(components);
  3. Place that import below the @import "tailwindcss" line, which is where Leed’s own stylesheet is injected.

That is the whole mechanism. Here is the before and after, on a real shape — restyling the documentation pager:

/* BEFORE — unlayered file, losing to Leed's layer(components) rule,
   so the author reached for !important. */
.leed-documentation .docs-pagination a .docs-pagination__label {
  color: var(--fg) !important;
}
/* AFTER — same rules, imported with layer(components) from site.config.css.
   Later in the same layer at ordinary specificity. No bang. */
@import "./docs/pager.css" layer(components);

/* tailwind/docs/pager.css */
.docs-pagination__label {
  color: var(--fg);
}

Your imports live in the entry file described in Tailwind Build, which is also where you can see the injection point for yourself.

Which of Leed’s CSS is layered, and which is not

Not everything Leed ships is layered, and the exceptions are deliberate.

ImportLayerWhyCan your layered CSS beat it?
@layer theme { tailwindcss/theme }themeTailwind’s own token blockYes — anything later beats theme
defaults.cssunlayered (declares its own @layer base / @layer utilities internally)Its :root token block must stay unlayered so your --site-* values are read by the var() fallback chain rather than fighting itNot applicable — it sets tokens, so you set tokens
components.csscomponentsThe 13 shared component stylesheetsYes — layer(components), imported after Leed
utils/utilities.cssunlayeredContains @utility rules, which cannot be nested in a layerOnly from an unlayered file of your own
fonts/all.cssunlayered@font-face blocksNot a contest — add your own faces
font-awesome/pro-7.3.1.cssunlayeredIcon font faces and utilitiesRarely wanted
highlighters.cssunlayeredThe 14 code-theme-* @utility rulesOnly from an unlayered file — or, better, by writing your own code theme
docs/themes.cssunlayeredThe 17 color-theme-* @utility rules plus docs-theme-defaultsOnly from an unlayered file — or, better, by setting the token
docs/docs.csscomponentsThe documentation layout, sidebar, TOC, pager, footer menuYes
@plugin "@tailwindcss/forms"pluginForm control resetsYes, from components
@plugin "@tailwindcss/typography"pluginprose classesYes, from components

Why themes are unlayered on purpose

Every color theme and every code theme is written as an @utility, and an @utility cannot be nested in a cascade layer. That is not a workaround — it buys tree-shaking. Utilities are emitted only when their class name is found in scanned source, so a site applying code-theme-github ships that one theme and not the other thirteen. Measured on a real build: 1 of 14 syntax themes and 0 of 17 documentation themes emitted. Turning them into plain classes so they could be layered would put all 31 into every site’s stylesheet to solve a problem a site can solve with one small unlayered file.

!important reverses the layer order

This is the one exception to everything above, and it is exact: for !important declarations, layer precedence runs backwards. An earlier layer beats a later one, and unlayered !important ranks lowest of all.

So an !important written by Leed from layer(components) cannot be beaten from an unlayered file at any specificity. The only way to beat it is to stand in the same layer, also carrying !important, and out-specify it.

Leed writes importance as Tailwind v4’s ! suffix — text-white!, hidden!, bg-transparent! — which is why grepping the source for the string !important under-reports it. Grep the built stylesheet instead.

The forced declarations that remain in Leed’s documentation CSS are mostly layout resets — margins, padding and display on nested elements. Two you may actually collide with:

  • docs/components/button.css writes text-white! on both the primary and secondary documentation buttons. A secondary button on a light ground is the case where that matters.
  • components/toc.css writes hidden! on .current-item, the caret marker in the “On this page” rail. That one is load-bearing — it is what keeps the marker hidden until a heading becomes current.

Most of the color forcings that used to sit on the sidebar and the pager have been deliberately removed, precisely so a site can restyle those rows from an ordinary rule. Do not reinstate them in your own CSS as a shortcut; they cost more than they buy.

Why Leed's imports used to be unlayered, and what broke

Leed’s stylesheet originally imported its component CSS with no layer annotation at all. The documented convention still told customers to write

@import "./site/components.css" layer(components);

which meant a customer’s three-class selector, sitting inside a layer, lost to Leed’s one-class selector sitting outside every layer — because unlayered beats layered. Every customer override needed !important, and once Leed also used !important the reversal rule made even that unwinnable from an unlayered file.

That was the cascade working exactly as designed. The bug was importing without a layer. components.css and docs/docs.css now import into layer(components), the layer order is declared before any import so it cannot drift, and only the three genuinely unlayerable things — @utility rules, @font-face, and defaults.css’s :root token block — remain outside.

Where theme utilities land, and what that means for docs chrome

docs-theme-defaults is the utility every color theme ends with (@apply docs-theme-defaults). It does four things: it sets the document’s text and background colors, it colors h1–h6 from the --component-header-N-color tokens, it paints .nav-logo and .logo-footer from --nav-logo-url, and it colors body links.

That last one is the sharp edge, because the utility is applied to <html>. An unscoped & a inside it would select every anchor in the document — your header nav, your footer columns, the sidebar, the breadcrumbs, the pager and the TOC. On a real documentation page that is around 320 anchors when about a dozen are body links. Because the utility lands in the utilities layer, no layered rule of yours could take them back at any specificity.

Leed scopes the rule to the article:

& #leed-docs-copy a { … }

#leed-docs-copy is the section that both doc-article.hbs and api-article.hbs wrap the page in. It holds the h1 and the prose and nothing else — breadcrumbs, pager, TOC, sidebar, header and footer are all outside it. It is also the stable hook: the inner content id differs by layout, because bravo passes contentId="content" to avoid colliding with its own wrapper.

The h1–h6 rules stay unscoped on purpose: no chrome element uses a heading tag, so they only ever match the article anyway.

To change the markup rather than the styling of the documentation header or footer, take ownership of the file with the eject flow in Overriding Leed Templates; the partials themselves are cataloged in Documentation Shell Partials.

The 13 component stylesheets you can override

components.css is one import per component, all inside layer(components). Knowing the names makes it much faster to find the one you mean:

alerts · api-methods · breadcrumbs · code · collapsible · document-search · form · kbd · pagination · scroll-status-indicator · tab-groups · tables · toc

Two of them carry a gotcha that belongs here rather than buried in the component’s own page.

Tabs: style the selected state on [aria-selected="true"]

The build stamps leed-tabs__item--active on the first tab of every container, and the browser script never moves it. activateTab moves only aria-selected and tabindex.

So a rule written against .leed-tabs__item--active paints the underline under whichever tab the reader started on and leaves it there for ever, no matter what they click. Write the rule against [aria-selected="true"] instead — which is also what a screen reader announces:

.leed-tabs__item[aria-selected="true"] {
  border-bottom-color: var(--primary);
  font-weight: 600;
}

Code blocks: the box belongs to Leed, the colors belong to your code theme

components/code.css already owns the geometry and both surfaces. pre:has(code.hljs) carries the radius, border, horizontal overflow, selection color and --component-code-bg-color; pre > code carries the inner padding and --component-code-inner-bg-color. There are two grounds because there are two boxes — an outer well and an inner code surface.

Two supported knobs exist for the copy button, so you do not have to fight its clearance: --code-copy-clearance (set it to 0 if you move the button out of the block’s top-right corner) and --code-copy-anchor (set it to static and take position: relative yourself if the button should live in your own chrome).

When a token beats an override

A three-branch decision rule, in the order you should try them:

What you want to changeWhat to write
A color on a Leed componentSet the matching --site-* token in your own :root
Structure — spacing, radius, layout, geometryA rule in a layer(components) file
Something painted by a theme utilitySet the --docs-* token — a layered rule cannot reach it

The full token list, with what each one colors and its light and dark defaults, is in Site Token Contract (--site-*).

Two files, opposite annotations, one reason

Later pages in this category tell you to import one file layer(components) and its neighbor unlayered, which reads as an inconsistency until you have the rule above. It is the same rule applied to two different opponents:

  • tailwind/partials/alerts.css is imported layer(components). Its opponent is Leed’s own alert component, which is in layer(components). Landing in the same layer, later, wins at identical specificity with no bang. See Theming Alerts.
  • tailwind/docs/mermaid.css is imported unlayered. Its opponent is the <style> block Mermaid writes inside each rendered SVG, which is author-origin and unlayered — and unlayered beats layered on normal declarations. Staying unlayered keeps you on equal footing with it. See Theming Diagrams.
ESC