Language

A .uxdsl file is CSS, plus token functions, breakpoint functions and four directives.

It is CSS

Anything that is not one of the constructs below passes through unchanged: selectors, properties, calc(),@media, @container, @layer, and native nesting (&:hover,.a { .b { } }), which is forwarded to the browser as written, not flattened. Names are case-insensitive (Space(1) compiles like space(1)). Every reference is checked against the effective theme when it is rewritten; an unknown one stops the build with a UXD_* code and a suggestion.

Token functions

Exactly one argument each — a key of the theme — plus an optional alpha on palette() and color(). They work in a declaration and inside a theme value.

FunctionCompiles toExampleWhat it is
palette(family[.variant][, alpha])var(--uxdsl__palette__family-variant)color: palette(primary.contrast);A semantic role; palette(primary) is palette(primary.main). Follows dark mode (modes.dark.palette).
color(family.shade[, alpha])var(--uxdsl__color__family-shade)background: color(gray.300, 0.5);A specific color of the collection, for a swatch or an identity. With an alpha (0–1) it compiles to color-mix(… transparent).
radius(k | pill | circle)var(--uxdsl__radius__k), 9999px, 50%border-radius: radius(2);A corner preset, or one of the two built-in shapes.
border(k)var(--uxdsl__border__k)border: border(1);A whole edge (width, style, color). Change the color or style locally with the longhands after it.
density(k)var(--uxdsl__density__k)padding: density(4);A spacing step that the theme moves at its breakpoints. The preferred way to space a component.
shadow(k)var(--uxdsl__shadow__k)box-shadow: shadow(2);A box-shadow preset; shadow(0) is none.
space(k)var(--uxdsl__space__k)gap: space(4);A step of the spacing scale, the same at every width. k is a key of the theme’s spacing, not a number of pixels.
.panel {
  padding: density(4);
  border: border(1);
  border-radius: radius(2);
  background: palette(surface.main);
  box-shadow: shadow(2);
}
.panel { border: border(1, red); }  /* UXD_EDGE_ARGUMENT: one argument; write the longhands after it */
.panel { color: palette(primary-main); }  /* UXD_PALETTE_SYNTAX: the one spelling is palette(primary.main) */

Responsive values

One function per breakpoint of the theme — xs() (from 0px), sm() (from 480px), md() (from 768px), lg() (from 1024px), xl() (from 1280px) with the base theme — wraps the value that applies from that width up. They compile to min-width media queries of the viewport; a theme that adds a breakpoint adds its function.

.layout {
  flex-direction: xs(column) md(row);
  padding: xs(1rem) xl(2rem) !important;
}
.hero { padding: md(2rem); }
  • A value applies until a later breakpoint overrides it: md(row) still applies at xl.
  • A lone group may start at any breakpoint (md(2rem) is “from md up”). When a value has other parts next to a group, every group needs a base, so the value has the same number of parts at every width.
  • !important goes after the groups and applies at every breakpoint.
  • A responsive value cannot nest in calc(), in another breakpoint, or under @keyframes, @font-face or @page; write the whole value per breakpoint, or put it on a custom property outside.
.a { padding: xs(1rem) xxl(2rem); }  /* UXD_BREAKPOINT_UNKNOWN: xxl is not a breakpoint of the theme */
.a { width: calc(100% - xs(1rem) md(2rem)); }  /* UXD_BREAKPOINT_CONTEXT: write xs(calc(100% - 1rem)) md(calc(100% - 2rem)) */
.a { padding: xs(1px !important) md(2px); }  /* UXD_BREAKPOINT_IMPORTANT: write xs(1px) md(2px) !important */
.a { padding: xs(); }  /* UXD_BREAKPOINT_EMPTY */

Directives

A directive applies a role from the theme to the rule it is written in. It must be a direct child of that rule — at the root of a file, or inside @media under the rule, it is UXD_DIRECTIVE_CONTEXT — and it is not itself responsive: put a responsive value on a property instead. Arguments are separated by spaces, the role first; a declaration written after the directive wins over the role’s.

DirectiveExampleWhat it applies
@ds-surface(role [tone] [size] [radius(k)] [shadow(k)]).card { @ds-surface(contained); }A container role: padding, radius, background, color, border and shadow. A tone swaps its colors for a Palette family; a size picks density(n) and radius(n).
@ds-typo(role).title { @ds-typo(h2); }A text role from typography_details: one declaration per field the theme defines for it, and nothing else.
@ds-button(role [tone] [size] [radius(k)] [shadow(k)]).save { @ds-button(contained primary); }An action role and its states (hover, active, focus, focusvisible, selected, disabled). Hover and active exclude a disabled control.
@ds-input(role [tone] [size] [radius(k)] [shadow(k)]).email { @ds-input(outlined); }A text-field role: the surface, caret, placeholder and states (hover, focus, focusvisible, readonly, invalid, disabled).
.notice { @ds-surface(outlined info 2); }
.save { @ds-button(contained success); border-radius: radius(pill); }
.cta { @ds-button(contained); @ds-button(outlined); }  /* UXD_DIRECTIVE_DUPLICATE: one control directive per rule */
.panel { @ds-surface(glass); }  /* UXD_SURFACE_REFERENCE: the role is not in the theme */

@uxdsl theme;

For a PostCSS-only setup (one plugin configuration for every stylesheet, as in Next.js without the CLI): written once, at the top level of the one global stylesheet, it is where the theme’s :root is emitted. The plugin then runs with includeTheme: false everywhere else. A misspelled, repeated or nested marker is UXD_THEME_MARKER. See the quick start.

The SCSS-style subset

The CLI, the Vite plugin and the Webpack loader read .uxdsl with this subset of Sass, and only this. The standalone PostCSS plugin reads plain CSS with $variables at the root of a file.

SupportedNotes
$variablesAt the root or in a block (block scope), !default, #{$x} in a selector, value or media query
@if / @elseNo @else if
@each $x in a, bLists only: no maps
@for $i from 1 through 3
@mixin / @include / @contentPositional arguments and defaults; an argument may be a token function or a responsive value. No keyword or variadic arguments
@import "./partial"A path, with or without the _partial convention
// commentsRemoved from the output

Anything else Sass has fails with a located error naming what to write instead: @extend, @use,@function, Sass color and math functions and arithmetic outside calc() are UXD_SCSS_UNSUPPORTED;&__item (concatenating the parent selector, which native nesting cannot do) is UXD_NESTING_INVALID.

$tones: primary, success;
@mixin pad($size) { padding: $size; }
@each $tone in $tones {
  .tag--#{$tone} { @ds-surface(contained #{$tone}); @include pad(density(2)); }
}

Errors the language can raise

Read from DIAGNOSTIC_CATALOG; what to do about each one is on Diagnostics.

CodeMeaning
UXD_SURFACE_ARGUMENT@ds-surface is not written as @ds-surface(role [tone] [size] [radius(k)] [shadow(k)]).
UXD_EDGE_REFERENCEradius(k) or border(k) names a token the theme does not define.
UXD_EDGE_ARGUMENTradius() or border() was given more than one argument.
UXD_SHADOW_REFERENCEshadow(k) names a token the theme does not define.
UXD_SHADOW_ARGUMENTshadow() was given more than one argument.
UXD_BUTTON_ARGUMENT@ds-button is not written as @ds-button(role [tone] [size] [radius(k)] [shadow(k)]).
UXD_INPUT_ARGUMENT@ds-input is not written as @ds-input(role [tone] [size] [radius(k)] [shadow(k)]).
UXD_TOKEN_KEYA token function's key is quoted, contains a dot where none is allowed, or is not a bare word.
UXD_TOKEN_ALPHAThe alpha of palette() or color() is not a number between 0 and 1.
UXD_SPACE_ARGUMENTspace() was given no argument or more than one.
UXD_SPACE_REFERENCEspace(k) names a Spacing key the theme does not define.
UXD_DENSITY_ARGUMENTdensity() was given no argument or more than one.
UXD_DENSITY_REFERENCEdensity(k) names a Density key the theme does not define.
UXD_PALETTE_ARGUMENTpalette() was given no argument or more than a key and an alpha.
UXD_PALETTE_REFERENCEpalette() names a family or variant the theme does not define.
UXD_PALETTE_SYNTAXA palette() argument uses the removed dashed spelling or is not family[.variant].
UXD_COLOR_ARGUMENTcolor() was given no argument or more than a key and an alpha.
UXD_COLOR_REFERENCEcolor() names a family or shade the theme does not define.
UXD_COLOR_SYNTAXA color() argument uses the removed dashed spelling or is not family[.shade].
UXD_SYNTAX_REMOVEDA spelling the language removed: rounded(), elevation(), densities() or radius(full).
UXD_THEME_BLOCK_REMOVEDA `@theme { … }` block: in-stylesheet token packs were removed.
UXD_OPTION_REMOVEDA plugin option that no longer exists was passed (a warning; the option is ignored).
UXD_DIRECTIVE_UNKNOWNAn at-rule in the reserved ds/ds-* namespace is not one of the four directives.
UXD_DIRECTIVE_CONTEXTA directive is not a direct child of the rule it styles (at the root, or under @media/@supports).
UXD_DIRECTIVE_DUPLICATEA second @ds-button or @ds-input in the same rule would emit competing states.
UXD_TYPO_ARGUMENT@ds-typo is not written as @ds-typo(role).
UXD_TYPO_REFERENCE@ds-typo names a typography role the theme does not define.
UXD_BREAKPOINT_UNKNOWNA top-level function is neither a configured breakpoint nor a known CSS function, next to a real breakpoint or one edit away from one.
UXD_BREAKPOINT_CONTEXTA breakpoint function is nested inside another function or breakpoint, or a responsive value sits under @keyframes/@font-face/@page/@counter-style.
UXD_BREAKPOINT_EMPTYA breakpoint function has no value, e.g. xs().
UXD_BREAKPOINT_IMPORTANT`!important` appears inside a breakpoint group.
UXD_BREAKPOINT_BASEIn a multi-part value, a responsive group has no value at the base breakpoint, so the value would change shape between widths.
UXD_VARIABLE_CONTEXTA `$variable` is declared inside a rule, which the standalone plugin does not scope.
UXD_VARIABLE_UNDEFINEDA `$name` in a value was never declared.
UXD_THEME_MARKER`@uxdsl theme;` is misspelled, written twice, or written inside a rule or an at-rule.
UXD_REFERENCE_MISSINGAn emitted var(--uxdsl__…) points at a custom property nothing defines.
UXD_REFERENCE_CYCLETheme tokens reference each other in a cycle.
UXD_REFERENCE_CONTEXTA `references` option cannot apply where it was given: an external token that is not a `--custom-property` name, or compiled `css` handed to generateThemeCss/validateTheme, which have no CSS parser.
UXD_SCSS_UNSUPPORTEDA Sass construct outside the subset, or one the subset cannot evaluate: @extend, @use, a %placeholder, !global, a Sass function, interpolation around a non-variable, an undefined $var or mixin, @else if, and/or/not, a keyword or variadic argument, an @import inside a block, or arithmetic outside calc().
UXD_NESTING_INVALIDA selector concatenates the parent with &-suffix, &__x or &--x, which native CSS nesting cannot express.
UXD_IMPORT_CYCLETwo or more .uxdsl files import each other in a cycle.
UXD_INCLUDE_ARGUMENTAn @include whose arguments do not parse (unbalanced parentheses, text after the list) or do not fit the mixin (too many, a missing one, a content block the mixin has no @content for).