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.
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.
| Function | Compiles to | Example | What 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) */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); }md(row) still applies at xl.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.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 */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.
| Directive | Example | What 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 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.
| Supported | Notes |
|---|---|
$variables | At the root or in a block (block scope), !default, #{$x} in a selector, value or media query |
@if / @else | No @else if |
@each $x in a, b | Lists only: no maps |
@for $i from 1 through 3 | |
@mixin / @include / @content | Positional 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 |
// comments | Removed 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)); }
}Read from DIAGNOSTIC_CATALOG; what to do about each one is on Diagnostics.
| Code | Meaning |
|---|---|
UXD_SURFACE_ARGUMENT | @ds-surface is not written as @ds-surface(role [tone] [size] [radius(k)] [shadow(k)]). |
UXD_EDGE_REFERENCE | radius(k) or border(k) names a token the theme does not define. |
UXD_EDGE_ARGUMENT | radius() or border() was given more than one argument. |
UXD_SHADOW_REFERENCE | shadow(k) names a token the theme does not define. |
UXD_SHADOW_ARGUMENT | shadow() 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_KEY | A token function's key is quoted, contains a dot where none is allowed, or is not a bare word. |
UXD_TOKEN_ALPHA | The alpha of palette() or color() is not a number between 0 and 1. |
UXD_SPACE_ARGUMENT | space() was given no argument or more than one. |
UXD_SPACE_REFERENCE | space(k) names a Spacing key the theme does not define. |
UXD_DENSITY_ARGUMENT | density() was given no argument or more than one. |
UXD_DENSITY_REFERENCE | density(k) names a Density key the theme does not define. |
UXD_PALETTE_ARGUMENT | palette() was given no argument or more than a key and an alpha. |
UXD_PALETTE_REFERENCE | palette() names a family or variant the theme does not define. |
UXD_PALETTE_SYNTAX | A palette() argument uses the removed dashed spelling or is not family[.variant]. |
UXD_COLOR_ARGUMENT | color() was given no argument or more than a key and an alpha. |
UXD_COLOR_REFERENCE | color() names a family or shade the theme does not define. |
UXD_COLOR_SYNTAX | A color() argument uses the removed dashed spelling or is not family[.shade]. |
UXD_SYNTAX_REMOVED | A spelling the language removed: rounded(), elevation(), densities() or radius(full). |
UXD_THEME_BLOCK_REMOVED | A `@theme { … }` block: in-stylesheet token packs were removed. |
UXD_OPTION_REMOVED | A plugin option that no longer exists was passed (a warning; the option is ignored). |
UXD_DIRECTIVE_UNKNOWN | An at-rule in the reserved ds/ds-* namespace is not one of the four directives. |
UXD_DIRECTIVE_CONTEXT | A directive is not a direct child of the rule it styles (at the root, or under @media/@supports). |
UXD_DIRECTIVE_DUPLICATE | A 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_UNKNOWN | A 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_CONTEXT | A breakpoint function is nested inside another function or breakpoint, or a responsive value sits under @keyframes/@font-face/@page/@counter-style. |
UXD_BREAKPOINT_EMPTY | A breakpoint function has no value, e.g. xs(). |
UXD_BREAKPOINT_IMPORTANT | `!important` appears inside a breakpoint group. |
UXD_BREAKPOINT_BASE | In a multi-part value, a responsive group has no value at the base breakpoint, so the value would change shape between widths. |
UXD_VARIABLE_CONTEXT | A `$variable` is declared inside a rule, which the standalone plugin does not scope. |
UXD_VARIABLE_UNDEFINED | A `$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_MISSING | An emitted var(--uxdsl__…) points at a custom property nothing defines. |
UXD_REFERENCE_CYCLE | Theme tokens reference each other in a cycle. |
UXD_REFERENCE_CONTEXT | A `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_UNSUPPORTED | A 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_INVALID | A selector concatenates the parent with &-suffix, &__x or &--x, which native CSS nesting cannot express. |
UXD_IMPORT_CYCLE | Two or more .uxdsl files import each other in a cycle. |
UXD_INCLUDE_ARGUMENT | An @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). |