* root: switch cspell to typo-only reporting cSpell's default mode reports every word it does not recognize, so each new integration name or identifier needed a dictionary entry. Switch to unknownWords: report-common-typos, which matches CodeSpell's behavior: only known misspellings (reported with a suggested fix) and flagWords fail the check. Remove the per-topic dictionaries and word-shape ignore regexes this makes unnecessary, fix the 18 typos the old allowlists were masking, and document the new workflow. Claude-Session: https://claude.ai/code/session_014ZuquWGxU5ReaXR5D3NmwW * web/docs: fix style-loader-plugin path and wording in CSS architecture doc Claude-Session: https://claude.ai/code/session_014ZuquWGxU5ReaXR5D3NmwW * website: remove stray blank lines left by ignore-comment removal Claude-Session: https://claude.ai/code/session_014ZuquWGxU5ReaXR5D3NmwW
5.3 KiB
title, sidebar_label, description
| title | sidebar_label | description |
|---|---|---|
| Cascade layers | Cascade layers | How authentik's web UI orders its CSS with @layer, and where each kind of rule belongs. |
authentik's document CSS is ordered with cascade layers. Layers make the ordering explicit, so a rule's precedence comes from the layer it is assigned to rather than from where it happens to be imported or how specific its selector is.
The layer order
One declaration, in web/src/styles/layers.css, defines the order for the whole document. It is the first import in every entrypoint:
@layer reset, vendor, components, theme, mode, brand;
Lowest precedence first:
| Layer | Holds |
|---|---|
reset |
Top-level normalization. Sparse — most of the reset is folded into PatternFly. |
vendor |
Vendored PatternFly and the bundled typefaces. Frozen; do not hand-edit. |
components |
Document-scope component rules, and the :root blocks bridging global tokens to component ones. |
theme |
The default design tokens. CSS custom property definitions only. |
mode |
Light/dark, high-contrast, and reduced-motion overrides of theme. |
brand |
Per-deployment branding overrides. |
layers.css is the only place the order is declared. Adding a layer anywhere else creates a second, independent ordering rather than extending this one.
Assigning a rule to a layer
Layers are applied only in the entrypoint files, on the import itself:
/* web/src/styles/interface.global.css */
@import "#styles/layers.css";
@import "#styles/global/vendor/patternfly.css" layer(vendor);
@import "@goauthentik/fonts/faces.css" layer(vendor);
@import "#styles/global/theme/variables.css" layer(theme);
@import "#styles/global/reset/globals.css" layer(reset);
@import "#styles/global/mode/mode.css" layer(mode);
@import "#styles/authentik/components/Placeholder/placeholder.css" layer(components);
The three entrypoints — interface.global.css (Admin and User), flows.global.css (Flow), and static.global.css (Django templates) — contain only @import statements. Individual stylesheets never wrap themselves in @layer, which keeps every precedence decision in one reviewable file per bundle.
Import order still matters within a single layer. Across layers it does not: a later layer always wins, regardless of import order or selector specificity.
Two cascade contexts
authentik has two independent cascades, and layers only govern the first.
The document. Driven by the three .global.css entrypoints, ordered by the layer stack above.
Each shadow root. Every component extending AKElement (web/src/elements/Base.ts) adopts, in order: shadow/patternfly-base.css, the component's own styles, shadow/authentik-base.css, and then brand custom CSS. These sheets are unlayered.
The bridge between the two is inheritance. CSS custom properties defined at the document level cross the shadow boundary, so a component reads --ak-color-primary without importing anything. What crosses is the computed value — whichever declaration won at the document level.
This has a consequence worth internalizing:
Layers order selectors, not properties.
Once a custom property crosses into a shadow root, the layer it was declared in no longer applies. Only its computed value carries over, and a declaration on :host inside the shadow root overrides what was inherited. Declare custom properties in consistent "dictionary" containers — :root at the document level, :host in shadow roots — so their specificity is predictable at the boundary.
Brand customization
Brands customize appearance two ways, and they land in different places:
- Custom properties from the database are injected into the reserved
brandlayer, the last in the order, so they override theme and mode without needing!important. - A custom CSS file is adopted per shadow root, after the component's own styles, so it can reach into
::part()surfaces.
Because the custom CSS path is adopted last within its shadow root rather than being layered, existing !important declarations in brand CSS keep working. They are usually unnecessary.
Adding CSS
- A new document-level component rule goes in
web/src/styles/authentik/components/<Name>/, imported into the relevant entrypoint withlayer(components). - A new design token goes in
web/src/styles/global/theme/, which is already imported intolayer(theme). - A light/dark or accessibility override goes in
web/src/styles/global/mode/. - Anything vendored from PatternFly goes through
web/src/styles/global/vendor/patternfly.css.
web/src/styles/README.md is the in-repo companion to this page, with the full directory layout and a "what is where" index.