Design: the CSS pipeline¶
This page records why the stylesheet build is shaped the way it is, and the
invariants that keep it safe to change. For day-to-day commands see the
repository’s AGENTS.md.
Context¶
Until v0.7 the stylesheet was authored in SCSS and compiled with dart-sass via npm — the only reason Node existed in the toolchain. An audit showed the Sass usage was shallow: 26 mechanical loops (spacing, grid columns × breakpoints, colour variants, borders, sizing, display, text utilities), two build-time colour computations (a contrast picker and a 15% darkening for hover shades), and concatenation + minification. There were no mixins and no deep nesting.
That shape had real costs: a Node dependency for a Python project (and the
perennial sass version-pin chore); hover shades baked at compile time, so user
overrides of --sd-color-* never reached them; and a “supported browsers”
list frozen in 2021.
Decision¶
The stylesheet is now plain CSS in two tiers:
Hand-authored component CSS —
style/*.css, one file per component area, written with expanded selectors.Generated utility CSS — emitted by
tools/generate_css.pyfrom declarative tokens instyle/design.toml(palette with precomputed contrast text colours, spacing scale, breakpoints, column count). A utility variant is a data entry, not a loop.
The generator concatenates both tiers in an explicit order, minifies with
rcssmin (a mature, frozen,
pure-Python minifier — deliberately not hand-rolled), and writes the single
committed artifact sphinx_design/static/sphinx-design.min.css. Only the
artifact ships; the generator, tokens and sources are development-only.
Alternatives considered: keeping dart-sass (permanent Node dependency for
three shallow features); libsass-python (upstream libsass is deprecated by
the Sass team); shipping unminified CSS (workable, but a free 5% wire saving
was kept by using an off-the-shelf minifier).
The hover shades are now computed in the browser:
--sd-color-*-highlight is declared twice — a static fallback first, then
color-mix(in srgb, black 15%, var(--sd-color-*)) — so overriding a theme
colour re-derives its hover shade at runtime, and browsers without
color-mix() keep the previous static value.
The cascade is ASSEMBLY order¶
tools/generate_css.py assembles the stylesheet from an explicit ASSEMBLY
list interleaving hand-authored files and generated families. This is a
deliberate choice over globbing: source order is the CSS cascade, and two
rules with equal specificity resolve by position. An automatic glob would
make the cascade an accident of filenames. The cost — remembering to register
new files — is converted into a loud failure by the completeness guard below.
This is not theoretical: during the migration, two real cascade-order
inversions (grid col-auto vs row-cols utilities, and the container
max-width media caps) were caught only because the verification tooling was
made order-aware. Any future restructuring of ASSEMBLY should be checked
with the equivalence tool’s order pass.
Invariants and their enforcement¶
Every invariant below is enforced by a named guard — if a guard is removed, its row here is no longer true.
Invariant |
Enforced by |
|---|---|
Every |
|
|
schema validation on every generator run, naming the offending key |
The committed artifact is never stale |
the |
The artifact always parses as valid CSS |
|
Generated utility families exactly match the token data |
|
The |
|
Output is byte-deterministic (across runs, Python versions, platforms) |
stdlib + frozen |
Migrations/restructurings preserve rule sets and cascade order |
|
Public API¶
The following are stable interfaces covered by deprecation policy:
the
--sd-*CSS custom property names;the
sd-*utility and component class names;the served filename
sphinx-design.min.css.
Everything else — the generator, design.toml’s schema, style/ file layout,
the checker — is internal and may change without notice.
Browser support¶
Styles target Baseline Widely Available web features; see the policy in
Getting started. Individual features may use Baseline
Newly Available CSS with graceful degradation (as color-mix() does above),
noted where they are documented.