Design decisions — spacing, type, color, borders, shadows, and how each of them changes at a breakpoint — live once, in a theme JSON. Components name them (density(4), palette(primary.main), @ds-button(contained)) and never write a breakpoint. The compiler turns that into ordinary CSS, refuses to emit anything it cannot resolve, and the same JSON is applied in the browser at run time and audited from the command line.
Theme uxdsl.theme.json, merged over the base theme
{
"densities": {
"4": "xs(space(4)) md(space(5)) xl(space(6))"
}
}UXDSL you write paradigm/card.uxdsl
.card {
@ds-surface(contained);
display: grid;
padding: density(4);
gap: xs(0.5rem) md(1rem);
}
.card h2 { @ds-typo(h2); }CSS you get compiled by uxdsl; :root shows only these tokens
:root { --uxdsl__typography__h2-font-size: var(--uxdsl__space__6); }
@media (min-width: 480px) { :root { --uxdsl__typography__h2-font-size: var(--uxdsl__space__7); } }
@media (min-width: 1024px) { :root { --uxdsl__typography__h2-font-size: var(--uxdsl__space__8); } }
@media (min-width: 1280px) { :root { --uxdsl__typography__h2-font-size: var(--uxdsl__space__9); } }
:root { --uxdsl__density__4: var(--uxdsl__space__4); }
@media (min-width: 768px) { :root { --uxdsl__density__4: var(--uxdsl__space__5); } }
@media (min-width: 1280px) { :root { --uxdsl__density__4: var(--uxdsl__space__6); } }
.card {
padding: var(--uxdsl__surface__contained-padding);
border-radius: var(--uxdsl__surface__contained-radius);
background: var(--uxdsl__surface__contained-bg);
color: var(--uxdsl__surface__contained-color);
border: var(--uxdsl__surface__contained-border);
box-shadow: var(--uxdsl__surface__contained-shadow);
display: grid;
padding: var(--uxdsl__density__4);
gap: 0.5rem;
}
@media (min-width: 768px) { .card { gap: 1rem; } }
.card h2 {
font-family: var(--uxdsl__typography__h2-font-family);
font-size: var(--uxdsl__typography__h2-font-size);
line-height: var(--uxdsl__typography__h2-line-height);
font-weight: var(--uxdsl__typography__h2-font-weight);
letter-spacing: var(--uxdsl__typography__h2-letter-spacing);
margin-block-start: var(--uxdsl__typography__h2-margin-block-start);
margin-block-end: var(--uxdsl__typography__h2-margin-block-end);
}density(4) is written once. The theme says it is space(4) on a phone, space(5) from md and space(6) from xl; the compiler writes that ladder once, as custom properties in :root and min-width media queries, and the card reads var(--uxdsl__density__4). Change the theme line and every component that names density(4) follows, with no component edited. The gap shows the other half: a component can still say what changes at a breakpoint, by naming the theme's breakpoints instead of writing pixel widths.
.uxdsl file is CSS, native nesting included, plus value functions (space(), density(), color(), palette(), radius(), border(), shadow(), and one per breakpoint, xs() … xl()), four directives (@ds-surface, @ds-button, @ds-input, @ds-typo) and a documented SCSS-style subset ($variables, @if/@else, @each over a list, @for, @mixin/@include, @content, @import). The Language page has the whole grammar.uxdsl build), the PostCSS plugin (uxdsl/postcss), the Vite plugin and the Webpack loader all run the same compile(). The output is plain CSS: custom properties and min-width media queries, nothing at run time.applyTheme, and audited with uxdsl theme --diff and uxdsl theme --contrast.UXD_* error with file, line, column and a suggestion — never a silent fallback. See Diagnostics.UXD_SCSS_UNSUPPORTED naming what to write instead.min-width queries: there are no container queries — use native @container next to UXDSL.clamp() scales (a clamp() is still a valid literal value).UXD_THEME_STRUCTURE instead of half-applied."It is CSS variables plus media queries; I can write that." Yes — that is literally the output. UXDSL generates and checks that ladder from one definition per token, keeps the definitions in JSON so that the build, the runtime, the contrast audit and coding agents read the same thing, and turns a var() that would resolve to nothing into a build error. With five tokens, write it by hand.
| Tool | Where a responsive value lives | What it has that UXDSL does not | What UXDSL adds |
|---|---|---|---|
| Tailwind CSS v4 | On every element: p-3 md:p-4 xl:p-6 | Container queries, arbitrary variants, a large ecosystem | The progression is defined once, on the token, and every component that names it follows |
| Open Props | In the token, as fluid clamp() scales; no build step | Fluid scales, zero tooling | Roles (surfaces, buttons, inputs), build-time validation, the theme as JSON |
| Panda CSS / vanilla-extract | In typed recipes and conditions; Panda can also express responsive tokens | Full typing, recipes, container-query conditions | Authoring in CSS with no code generation; an optional runtime; diagnostics with file and line |
| Sass | Wherever you write the mixin | A complete language | Theme tokens and roles with references checked against a schema; UXDSL does not pretend to be a language |
| Style Dictionary / DTCG | In the token pipeline, exported to many platforms | The DTCG format, export to iOS, Android and design tools | The consumption side in CSS; its JSON is its own format, not DTCG |
Container queries, fluid clamp() scales, DTCG import and export, modes other than dark, and editor completion of a project's own theme are recorded as possible additions. None is promised.