A theme is a partial override of the base theme that ships in the package (uxdsl/theme/base.json): write only what you change. These are every family the schema knows, with the keys the base theme defines.
| Family | Shape | In the base theme | What it is | Read by |
|---|---|---|---|---|
breakpoints | name → width in px (a number) | xs 0, sm 480, md 768, lg 1024, xl 1280 | The viewport widths, in px, where responsive values step. The base (xs) must be 0. | xs() … xl() and every responsive value in the theme |
spacing | key → value | 0–16 | The spacing scale: a key per step, one value each, the same at every width. | space(k); densities, radii and typography reference it |
palette | family → variant → value | primary, secondary, surface, tertiary, success, info, warning, error, dark, … (14) | Semantic roles (primary, surface, text, …), each a family of variants (main, light, dark, contrast, …). | palette(family.variant); every role and directive |
fonts | families: name → stack; google: [family spec] | google, families | families: the font stacks typography roles name; google: the Google Fonts families to @import ([] opts out). | --uxdsl__font__name; typography_details |
colors | family → shade → color, or name → color | white, black, gray, purple, pink, red, amber, green, sky | The color collection: families of shades (gray.50 … gray.950) or single named colors. | color(family.shade); palette values reference it |
typography_details | role → fontFamily, fontSize, lineHeight, fontWeight, letterSpacing, textTransform, textDecoration, fontStyle, marginBlockStart, marginBlockEnd | default, tag, h1, h2, h3, h4, h5, h6, p, … (17) | Text roles (default, h1 … h6, p, caption, …): each field a CSS value or a responsive expression; default fills the fields a role omits. | @ds-typo(role) |
densities | key → value | 0–15 | Spacing steps that move at breakpoints: each value is usually a responsive expression over space(). | density(k); surface sizes |
inputs | role → surface, base (padding, radius, bg, color, border, shadow, opacity, outline, outline-offset, transform, cursor, font-weight, caret, placeholder, underline), states (hover, focus, focusvisible, readonly, invalid, disabled) | contained, outlined, underline | Text-field roles: a surface, base overrides (caret, placeholder, underline …) and per-state fields. | @ds-input(role) |
buttons | role → surface, base (padding, radius, bg, color, border, shadow, opacity, outline, outline-offset, transform, cursor, font-weight), states (hover, active, focus, focusvisible, disabled, selected) | contained, outlined, flat | Action roles: a surface, base overrides and per-state fields. | @ds-button(role) |
surfaces | role → padding, radius, bg, color, border, shadow | contained, outlined, flat | Container roles composed from the families above: padding, radius, bg, color, border, shadow. | @ds-surface(role); buttons and inputs build on a surface |
shadows | key → value | 0–5 | box-shadow presets, static or responsive, layers allowed. shadow(0) is none. | shadow(k); surfaces |
borders | key → value | 0–5 | Whole-edge presets (width, style, color), static or responsive. border(0) is none. | border(k); surfaces |
radii | key → value | 0–5 | Corner presets, static or responsive. radius(0) is square; pill and circle are built in. | radius(k); surfaces and sizes |
modes | dark → palette → (as palette) | dark | Dark mode: modes.dark.palette redefines palette values, and only those; nothing else has a dark variant. | the same palette() references, under prefers-color-scheme and data-theme |
uxdsl.theme.json next to uxdsl.config.cjs (or in the working directory) — or uxdsl.theme.js / .cjs exporting the theme object when it is computed. The CLI, the PostCSS plugin, the Vite plugin and the Webpack loader discover the same file; a theme option passed to one of them wins. Point $schema at the packaged schema and an editor validates and completes it:
{
"$schema": "./node_modules/uxdsl/schema/theme.schema.json",
"breakpoints": { "md": 800 },
"palette": { "primary": { "main": "color(green.700)" } }
}Objects merge key by key; a string — a responsive expression included — replaces the whole value. This override and the result deepMergeTheme gave when this page was built:
{
"palette": {
"primary": {
"main": "color(green.700)"
}
},
"spacing": {
"4": "0.875rem"
}
}palette.primary → {"main":"color(green.700)","light":"color(purple.500)","dark":"color(purple.900)","contrast":"color(white)"}
spacing["4"] → "0.875rem" spacing["5"] → "1rem"uxdsl theme prints the effective theme and uxdsl theme --diff labels every leaf project or default (Tooling).
Every leaf is a nonempty string (a breakpoint width is a number), written in one grammar on every path:
"0.75rem", "0 1px 2px rgba(0, 0, 0, 0.1)";space(4), color(gray.300), palette(surface.light), radius(2), border(1), shadow(2);xs(space(4)) md(space(5)) — not in spacing, colors, palette or fonts.families;var(--…) as the escape hatch.validateTheme (uxdsl/theme) is the one validator: the plugin, the CLI, generateThemeCss and applyTheme all call it, and nothing is coerced. A number where a string belongs, as this page’s build found:
UXD_THEME_INVALID breakpoints.md: UXD_THEME_INVALID: Expected a finite number of pixels, got a string (at breakpoints.md).
UXD_THEME_INVALID spacing.4: UXD_THEME_INVALID: Expected a nonempty string, got number 12 (at spacing.4).modes.dark.palette redefines palette values for dark mode, and nothing else has a dark variant. The compiled CSS applies them under prefers-color-scheme: dark unless <html data-theme="light">, and always under data-theme="dark". Components keep naming palette(…); a switch only sets the attribute:
document.documentElement.setAttribute('data-theme', 'dark') // or 'light'; remove it to follow the OSThe compiled ladder is on Colors & palette; a dark key you omit keeps its light value.
fonts.google lists Google Fonts families; the compiled stylesheet and generateThemeCss lead with their @import, before every other rule. The base theme’s, encoded by googleFontsImportUrls:
https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap{ "fonts": { "google": [] } } opts out: no import, and no request to Google. fonts.families names the stacks (--uxdsl__font__ui, --uxdsl__font__code).
resolveTheme(override) and DEFAULT_THEME| Variant | resolveTheme | From | DEFAULT_THEME |
|---|---|---|---|
main | #0f766e | your override | color(purple.700) |
light | color(purple.500) | base | color(purple.500) |
dark | color(purple.900) | base | color(purple.900) |
contrast | color(white) | base | color(white) |
structuralChanges(themeStructure(…)): values only, so applyTheme would apply it without a rebuild
Override only main and dark/contrast come from the base: the same merge uxdsl theme --diff labels leaf by leaf (see the CLI page).
tokenValueToCss · validateResponsiveExpression · analyzeResponsiveValuetokenValueToCss → color-mix(in srgb, var(--uxdsl__palette__primary-main) 50%, transparent)
| Breakpoints | Base | !important |
|---|---|---|
xs md | yes | no |
googleFontsImportUrls · encodeGoogleFontFamilyThe active theme's fonts.google, as the @import URLs the compiler and generateThemeCss both emit:
https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap encodeGoogleFontFamily → Noto+Sans+JP:wght@400;700
The theme this site is showing. Edit it and the page follows through applyTheme; a change of structure is refused. Nothing is saved to a file.