# Generating and validating a theme
Generating a theme with Dynamic's tools is an optional workflow: the result is the same variable stylesheet that Theming describes, written from a short JSON file instead of by hand. Since version 2.12.0, @dynamic-framework/ui-react includes two commands: dynamic-theme-expand, which turns the JSON into CSS, and dynamic-theme-validate, which checks a theme CSS file.
# When it pays off
- A rebrand of several roles, where writing 12 variables per role by hand is slow and error-prone.
- A theme with zones, which adds per-component blocks inside each one.
- A theme that is maintained over time and regenerated with every Dynamic version.
For a one-off adjustment, such as a radius or a link color, editing the variable stylesheet is enough.
# What it assumes
- Node.js 22 or later on the machine of whoever builds the theme. The site doesn't need Node.
- A source JSON under version control: that is what gets edited.
- A generated CSS file that is uploaded to the site like any other stylesheet, loaded after the Dynamic CSS. That CSS isn't edited by hand: it is regenerated from the JSON.
# Installation and usage
npm i -D @dynamic-framework/ui-react@2.12.1
npx dynamic-theme-expand theme.json -o theme.css
npx dynamic-theme-validate theme.css --strict
Without -o, dynamic-theme-expand writes the CSS next to the JSON, with the same name and a .css extension. If the JSON has errors, the command lists them all, exits with code 1 and doesn't write the CSS. dynamic-theme-validate exits with code 1 when it finds errors, so it can run in CI.
The CSS starts with a header that states which @dynamic-framework/ui-react version it was generated for.
# The input JSON
{
"name": "Example theme",
"version": "1.0.0",
"roles": { "primary": "#166e62" },
"body": { "bg": "245, 246, 250", "color": "21, 21, 26" },
"typography": {
"fontFamily": "\"Inter\", sans-serif",
"scale": { "1": "2.75rem", "2": "2.25rem" }
},
"radius": "0.5rem"
}
Colors accept hex, an "R, G, B" triplet or any CSS color.
| Key | Required | What it is |
|---|---|---|
name, version | no | Header of the generated CSS |
roles.<role> | at least one role, or gray | Base color of primary, secondary, success, info, warning, danger, light or dark. Generates the base, the ramp (if the role has one) and, except for secondary, the text of .text-bg-<role> |
gray | no | Derives the gray ramp from one color. It changes the character of the neutral, not just its hue |
body.bg, body.color | in a full theme | Document background and text |
body.borderColor | no | Border color. Defaults to Dynamic's |
typography.fontFamily | in a full theme | The font family. Emitted as --bs-font-sans-serif |
typography.scale.<1-6> | no | Heading size per step, in rem. Fluid steps come out with their cap in @media (min-width: 1200px) |
radius | in a full theme | Base radius in rem. sm, lg, xl and xxl are derived from it (×0.5, ×2, ×3, ×4) |
root | no | Map of --bs-* → value, emitted as is in the root block |
components | no | List of { selector, vars, declarations }: one block per selector. declarations only accepts padding, border-radius, border-color, font-family and font-variant-numeric |
zones | no | Map of <name> → { vars, nav, components }: a [data-bs-theme="<name>"] block, one block for .nav and another for .nav-pills, and one block per component inside the zone. In vars the triplets are enough: the generator emits the wrapper of each triplet the zone redefines right after it, and emits color and background-color when the zone redefines the body background or text triplet (one of the two is enough) |
Without body, typography and radius, the theme is partial: see A single role.
Every var(--bs-…) reference in root, components and zones is checked against the variables that exist in the CSS of the installed version. A reference to a variable that doesn't exist is an error.
# A single role
A JSON without body, typography and radius generates only the ramps of the listed roles, in a block to add to an existing theme stylesheet. It accepts roles, gray, name and version.
{ "roles": { "primary": "#166e62" } }
/*
* Para @dynamic-framework/ui-react 2.12.1.
* Bloque parcial para incorporar a una hoja de theme existente.
* Se carga después del CSS de Dynamic UI.
*/
:root,
[data-bs-theme="dynamic"] {
/* Role primary */
--bs-primary-rgb: 22, 110, 98;
--bs-primary-text-bg-color: var(--bs-white);
--bs-primary-25-rgb: 243, 248, 247;
--bs-primary-50-rgb: 232, 241, 239;
--bs-primary-100-rgb: 208, 226, 224;
--bs-primary-200-rgb: 162, 197, 192;
--bs-primary-300-rgb: 115, 168, 161;
--bs-primary-400-rgb: 69, 139, 129;
--bs-primary-500-rgb: var(--bs-primary-rgb);
--bs-primary-600-rgb: 18, 88, 78;
--bs-primary-700-rgb: 13, 66, 59;
--bs-primary-800-rgb: 9, 44, 39;
--bs-primary-900-rgb: 4, 22, 20;
}
The leaves come from mixing the base in sRGB with white (95, 90, 80, 60, 40 and 20 % for steps 25 to 400) and with black (20, 40, 60 and 80 % for steps 600 to 900), the same recipe Dynamic uses for its palette.
# What it validates
dynamic-theme-validate checks any theme CSS, generated or written by hand. Each contrast is measured in its context: the root block, each component block and each zone. The threshold is 4.5:1 and is changed with --min-contrast.
| Severity | Rules |
|---|---|
| Error | vacio, triplete, ciclo, wrapper, rampa-incompleta, paso-500, superficie-faltante, border-color, where, tipografia, tipografia-breakpoint, monotonia, contraste-boton, contraste-nav-pills, contraste-zona, contraste-horneado, contraste-enlace-zona below 3:1 |
| Warning | wrapper-faltante-zona, rampa-inexistente, rampa-irresoluble, tipografia-familia, contraste-irresoluble, contraste-superficie, zona-pegada, contraste-preexistente, contraste-horneado on .text-bg-secondary, contraste-enlace-zona between 3:1 and 4.5:1 |
| Note | superficie-heredada, literal |
With errors, the command exits with code 1. With --strict, it also exits with code 1 when there are warnings; notes don't change the exit code.
# The token-overrides.json descriptor
token-overrides.json describes which Dynamic variables can be overridden and how they behave. It is published per version, next to the CSS of that same version: https://cdn.dynamicframework.dev/assets/2.12.1/ui-react/tokens/token-overrides.json. It is meant for the tools and agents that generate themes. dynamic-theme-expand and dynamic-theme-validate don't download it: they check variable names against known-tokens.json, the inventory that ships in the installed package. A hand-written theme doesn't need the descriptor.
Each variable carries a class:
| Class | What it means |
|---|---|
override-base | The -rgb base of a role. It drags the wrapper and step 500 along, but not the rest of the ramp |
override-leaf | A literal value. Overridden one by one; it doesn't follow the base |
follows | An alias or wrapper that recomputes itself. Not overridden |
not-overridable | A value inside a data-URI. Replace the whole variable |
# Resources
- Theming: the contract the generated CSS follows.
token-overrides.json2.12.1 (opens new window)- Dynamic UI 2.12.1 Storybook (opens new window)