Review brief: CSS¶
Try it yourself
Everything in this brief runs on the demo site: Drupal 11.5.0-beta1 with the module, its adapters, the Media Library pilot and demo content. You need DDEV 1.25 or newer:
git clone https://github.com/d34dman/design_system_api_demo.git
cd design_system_api_demo
ddev start && ddev install-demo # prints a one-time login link
Why this reaches your area¶
Everything the contract delivers to the page is CSS: custom properties named
--ds-*, grouped in a cascade layer, with blocks for dark mode and more contrast.
Module CSS built on it must use those properties instead of literal colors, and
logical properties instead of physical ones. Two stylelint rules catch literal colors
and physical properties, though not every way around them yet (see Known issues).
These are choices about how Drupal CSS should be written, so they need people who
shape Drupal's CSS.
Questions for you¶
- Naming. Tokens become
--ds-<group>-<element>-<role>-<state>, for example--ds-color-border-primaryor--ds-media-card-border-selected. Is--ds-a prefix core would accept for a shared vocabulary, and does the grammar fit core's CSS conventions? - Context blocks instead of
light-dark(). Dark mode and more contrast are separate blocks (@media (prefers-color-scheme: dark)plus[data-ds-mode="dark"]) holding only the values that differ. We chose this overlight-dark()because it also works for non-color tokens and for a single element marked dark inside a light page. That last part needs the theme's values in the contract: tokens an adapter maps to a theme's own variables (Default Admin) get no dark block, so such an element doesn't flip. Do you agree with the trade-off? → §4 step 3, Q6 - Cascade layers. The values sit in
@layer ds.context; module and theme CSS stay unlayered. Should Drupal define a layer order for core, themes and modules, and where would this layer go in it? - Delivery. The resolved values are one generated file per theme
(
public://design-system/<theme>-<hash>.css), added to every page with a<link>rather than through a library, so it isn't aggregated. Would you rather see it as a library, inline, or in the aggregate? - Linting.
tooling/stylelinthas two rules:drupal-ds/color-tokens-onlyanddrupal-ds/logical-properties. Is that the right minimum for contrib CSS, and should rules like these eventually live in core's stylelint config? → §8, Q12
What we built¶
- Generated CSS (
src/ContextCss.php), in one cascade layer:
@layer ds.context {
@layer base, contrast, mode, mode-contrast; /* on one element: later wins, whatever the specificity */
@layer base {
:root { --ds-color-border-primary: #1d4ed8; /* every token */ }
[data-ds-mode="light"] { color-scheme: light; /* base values back inside a dark page */ }
}
@layer contrast {
@media (prefers-contrast: more) { :root { /* only the values that differ */ } }
[data-ds-contrast="more"] { /* … */ }
}
@layer mode {
@media (prefers-color-scheme: dark) { /* only if the theme has dark mode */
:root:not([data-ds-mode="light"]) { /* only the values that differ */ }
}
[data-ds-mode="dark"] { color-scheme: dark; /* the same values, on demand */ }
}
@layer mode-contrast {
/* dark + more contrast: on one element, nested either way, or one from the OS */
@media (prefers-color-scheme: dark) and (prefers-contrast: more) { :root:not([data-ds-mode="light"]) { /* … */ } }
[data-ds-mode="dark"][data-ds-contrast="more"],
[data-ds-mode="dark"] [data-ds-contrast="more"],
[data-ds-contrast="more"] [data-ds-mode="dark"] { /* … */ }
}
}
The layer order only decides between blocks that match the same element. A nested
region declares its own values, and those beat inherited values whatever their
layer, so dark plus more contrast has its own combined context in the last
sub-layer, matched however the two meet (a token without a combined value takes
its dark value). A theme can name its own dark selector in place of
prefers-color-scheme (Default Admin: :root.dark-mode). The sample in concept
§4 step 3 is older than this code: it has no sub-layers and no combined context,
lists contrast after mode, and mentions @scope, which isn't generated. The block
above is what ships.
- The Media Library pilot's CSS (
modules/design_system_api_media_library/css/media-library.css, 467 lines) replaces about 1,700 lines of theme-specific Media Library CSS in Default Admin and Claro. It contains no hex values and no theme names, uses logical properties only, and has aforced-colorsblock; transitions run only underprefers-reduced-motion: no-preference. FunctionalJavascript tests cover the selection limit, the focus ring on a selected card and the grid layouts. Dialog chrome mostly uses two-class selectors, so it beats jQuery UI's base theme and ties with a theme's own two-class dialog rules, which load later and win. A one-class theme rule loses, such as Default Admin's.ui-dialog. Two rules use three classes on purpose, so the Media Library's own button-pane layout wins over a theme's dialog layout. - The linter's rules match rules in
design_system_api.rules.yml, where each rule has a level, a summary and the check that enforces it. Allowed:transparent,currentColor, CSS-wide keywords, literal colors asvar()fallbacks, and system colors inside@media (forced-colors: active).
See it¶
ddev drush ds:css --theme=olivero # the generated CSS
ddev ds-lint # the pilot passes; a seeded file fails

Olivero in Arabic (RTL), pilot on: the same stylesheet, through logical properties.
Known issues¶
Found in our own check of the code. Items marked Fixed name the commit that fixed them. We track the open ones ourselves; if one matters for your review, say so in this brief's issue.
- Fixed (
bb2a125,77237c5): Nested contexts. A contrast region inside a dark page (or the reverse) gets the combined dark + more-contrast values, and FunctionalJavascriptContextCascadeTestchecks the nested cases in a browser. A[data-ds-mode="light"]region on an OS-dark page with OS more contrast keeps the contrast values. - Fixed (
bb2a125): Dark + more-contrast context. New combined contextmode=dark contrast=morewith defaults for the contrast tokens; KernelContrastPairsTestchecks every text (4.5:1) and UI (3:1) pair in every context. Adapter themes still keep their own values in every context. - Fixed (
bb2a125):color-scheme. Explicit[data-ds-mode="dark"]and[data-ds-mode="light"]now set it, so native controls andlight-dark()values follow. - Fixed (
14a82bd):drupal-ds/color-tokens-onlygaps. It used to pass anyvar(), not only--ds-*(var(--gin-primary)); a made-up--ds-*name with a literal fallback; custom property declarations, including--ds-color-fg-default: #f00;filter: drop-shadow(… red); deprecated system colors such asWindowText; and system colors under@media not (forced-colors: active), and rejected relative color syntax built from tokens. Each of these is now checked;drupal-ds/custom-propertiescoversvar()names, and size, font and motion values have rules of their own. - Fixed (
1e4e30e):drupal-ds/logical-propertiesgaps. Physical shorthands whose inline sides differ (margin: 0 1rem 0 2rem,inset,border-width,border-radius) and physical values (background-position: left,linear-gradient(to right …),overflow-x,translateX()) are rejected.widthandheightare still rejected outright; a deliberate physical box takes a one-line exception with a reason. - Fixed (
bbadea9,ed82558): Not in the module's CI. The pipeline runs the plugin's tests and lints all module CSS with it; a violation fails the pipeline. - Fixed (
b8ffdc0): Pilot CSS. The focus ring now shows on selected cards (selected styles in:where()) and only for keyboard focus (:has(:focus-visible)); disabled items take no clicks past the limit (pointer-events: none); the card transition runs only underprefers-reduced-motion: no-preference(new must rulea11y/reduced-motion).
Read only these sections¶
What we heard¶
Nothing yet.
Edit note (2026-10-03): Corrected the pilot CSS line count (443), the dialog specificity description (two rules use three classes; one-class theme rules lose), the layer-order comment (it holds on one element, not across nesting), the Q2 claim for adapter-aliased tokens, and "two stylelint rules enforce that", and noted that the concept §4 step 3 sample is out of date; added Known issues. All found in a pre-review check of the code.
Edit note (2026-10-03, later): Updated for bb2a125 (combined dark + more-contrast layer, color-scheme, nesting tests), b8ffdc0 (pilot focus, disabled items, reduced motion, tests; 467 lines) and e37d733 (Default Admin's dark selector); marked the fixed Known issues. Open known issues are now tracked by the maintainers instead of as separate issues.
Edit note (2026-10-18): Marked fixed: nested contexts (77237c5), the drupal-ds rule gaps (14a82bd, 1e4e30e) and the rules in CI (bbadea9, ed82558); the demo site now runs Drupal 11.5.0-beta1 (the module supports ^11.5 || ^12).