Responsibility: define reusable visual action roles and their interaction states. A Button selects a Surface for its container treatment, then adds shared base overrides and states. Components choose the role; the theme owns its visual decisions. Native HTML and application code own behavior and accessibility.
Surfaces describe containers. Buttons add stateful action styling. Both read the theme JSON and share the same token and responsive engines. Preserve intent, not just the current computed value.
{
"breakpoints": {
"xs": 0,
"md": 768
},
"buttons": {
"checkout": {
"surface": "contained",
"base": {
"padding": "density(2)",
"shadow": "shadow(1)"
},
"states": {
"hover": {
"bg": "palette(primary.dark)"
},
"focusvisible": {
"outline": "2px solid palette(primary.main)",
"outline-offset": "3px"
},
"selected": {
"shadow": "xs(shadow(1)) md(shadow(3))"
},
"disabled": {
"opacity": "0.5",
"cursor": "not-allowed"
}
}
}
}
}This excerpt assumes its referenced tokens exist. Default roles are contained, outlined and flat. A custom role inherits contained defaults unless its configuration overrides them. Its surface selects an existing Surface; base overrides that composition; states overrides matching state fields. Each supplied string replaces its entire responsive expression.
.checkout { @ds-button(checkout); }
/* Use a real <button type="button"> for an action. */.checkout {
padding: var(--uxdsl__button__checkout-base-padding);
box-shadow: var(--uxdsl__button__checkout-base-shadow);
/* Other fields reference the selected Surface. */
}
.checkout:focus-visible {
outline: var(--uxdsl__button__checkout-focusvisible-outline);
outline-offset: var(--uxdsl__button__checkout-focusvisible-outline-offset);
}At md, the example selected shadow changes to shadow-3 and persists until overridden. Padding follows Density independently. The component retains references instead of copying the current pixels. Replace managed theme CSS with generateThemeCss(nextTheme) to update existing token values. Adding or removing state fields, changing the selected Surface, or changing role structure requires regenerating component CSS too; buttonComponentCss does this in the demo.
.save { @ds-button(contained primary 2); }
.special { @ds-button(outlined); border-style: dashed; }An optional configured Palette family overrides Surface colors. Default state colors follow the tone through shared references; explicitly configured Palette references keep their own meaning. A numeric size selects density(n) and radius(n), not pixels. Explicit Button base overrides take precedence over the Surface composition, including its tone and size. Use local CSS after the directive for a deliberate exception.
Supported visual fields are padding, radius, bg, color, border, shadow, opacity, outline, outline-offset, transform, cursor and font-weight. Supported states are hover, active, focus, focusvisible, disabled and selected. Selected maps to .is-selected, aria-pressed=true or aria-selected=true; use only semantics appropriate to the actual element. Disabled styling also recognizes aria-disabled, which does not itself prevent activation. Default packs provide hover and selected styling; define other treatments as needed and preserve browser focus indicators.
Palette contrast tokens are assignments, not automatic contrast guarantees. Verify foreground/background combinations, keyboard focus, disabled behavior and touch use. Styling does not implement click handling or toggle state.
@theme {
button-checkout: {
@ds-surface(contained);
padding: density(2);
:hover { bg: palette(primary-dark); }
:focusvisible { outline: 2px solid palette(primary-main); }
}
}
.checkout { @ds-button(checkout); }JSON overrides matching legacy base and state fields, followed by shared defaults. Import legacy definitions in each compilation; Button packs no longer leak through a global cache. PostCSS, runtime generation, inspection and this preview use the same Button engine. Defaults generate the legacy default file. Unknown roles, fields, states and invalid responsive mappings fail clearly; inspect token dependencies and actual CSS because validation is not a complete CSS or accessibility audit.
Actual viewport: 0px. Resize the browser to test responsive values. Hover, focus with the keyboard, or press a preview button to inspect configured states. JSON edits are scoped to this demo and do not save source files.
.action { @ds-button(contained); }.action { 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); }
.action:hover { background: var(--uxdsl__button__contained-hover-bg); color: var(--uxdsl__button__contained-hover-color); }
.action.is-selected, .action[aria-pressed="true"], .action[aria-selected="true"] { background: var(--uxdsl__button__contained-selected-bg); color: var(--uxdsl__button__contained-selected-color); }{
"--uxdsl__button__contained-hover-bg": "var(--uxdsl__button__tone-dark, var(--uxdsl__palette__primary-dark))",
"--uxdsl__button__contained-hover-color": "var(--uxdsl__button__tone-contrast, var(--uxdsl__palette__primary-contrast))",
"--uxdsl__button__contained-tone-primary-hover-bg": "var(--uxdsl__palette__primary-dark)",
"--uxdsl__button__contained-tone-primary-hover-color": "var(--uxdsl__palette__primary-contrast)",
"--uxdsl__button__contained-tone-secondary-hover-bg": "var(--uxdsl__palette__secondary-dark)",
"--uxdsl__button__contained-tone-secondary-hover-color": "var(--uxdsl__palette__secondary-contrast)",
"--uxdsl__button__contained-tone-surface-hover-bg": "var(--uxdsl__palette__surface-dark)",
"--uxdsl__button__contained-tone-surface-hover-color": "var(--uxdsl__palette__surface-contrast)",
"--uxdsl__button__contained-tone-tertiary-hover-bg": "var(--uxdsl__palette__tertiary-dark)",
"--uxdsl__button__contained-tone-tertiary-hover-color": "var(--uxdsl__palette__tertiary-contrast)",
"--uxdsl__button__contained-tone-success-hover-bg": "var(--uxdsl__palette__success-dark)",
"--uxdsl__button__contained-tone-success-hover-color": "var(--uxdsl__palette__success-contrast)",
"--uxdsl__button__contained-tone-info-hover-bg": "var(--uxdsl__palette__info-dark)",
"--uxdsl__button__contained-tone-info-hover-color": "var(--uxdsl__palette__info-contrast)",
"--uxdsl__button__contained-tone-warning-hover-bg": "var(--uxdsl__palette__warning-dark)",
"--uxdsl__button__contained-tone-warning-hover-color": "var(--uxdsl__palette__warning-contrast)",
"--uxdsl__button__contained-tone-error-hover-bg": "var(--uxdsl__palette__error-dark)",
"--uxdsl__button__contained-tone-error-hover-color": "var(--uxdsl__palette__error-contrast)",
"--uxdsl__button__contained-tone-dark-hover-bg": "var(--uxdsl__palette__dark-dark)",
"--uxdsl__button__contained-tone-dark-hover-color": "var(--uxdsl__palette__dark-contrast)",
"--uxdsl__button__contained-tone-neutral-hover-bg": "var(--uxdsl__palette__neutral-dark)",
"--uxdsl__button__contained-tone-neutral-hover-color": "var(--uxdsl__palette__neutral-contrast)",
"--uxdsl__button__contained-tone-light-hover-bg": "var(--uxdsl__palette__light-dark)",
"--uxdsl__button__contained-tone-light-hover-color": "var(--uxdsl__palette__light-contrast)",
"--uxdsl__button__contained-selected-bg": "var(--uxdsl__button__tone-dark, var(--uxdsl__palette__primary-dark))",
"--uxdsl__button__contained-selected-color": "var(--uxdsl__button__tone-contrast, var(--uxdsl__palette__primary-contrast))",
"--uxdsl__button__contained-tone-primary-selected-bg": "var(--uxdsl__palette__primary-dark)",
"--uxdsl__button__contained-tone-primary-selected-color": "var(--uxdsl__palette__primary-contrast)",
"--uxdsl__button__contained-tone-secondary-selected-bg": "var(--uxdsl__palette__secondary-dark)",
"--uxdsl__button__contained-tone-secondary-selected-color": "var(--uxdsl__palette__secondary-contrast)",
"--uxdsl__button__contained-tone-surface-selected-bg": "var(--uxdsl__palette__surface-dark)",
"--uxdsl__button__contained-tone-surface-selected-color": "var(--uxdsl__palette__surface-contrast)",
"--uxdsl__button__contained-tone-tertiary-selected-bg": "var(--uxdsl__palette__tertiary-dark)",
"--uxdsl__button__contained-tone-tertiary-selected-color": "var(--uxdsl__palette__tertiary-contrast)",
"--uxdsl__button__contained-tone-success-selected-bg": "var(--uxdsl__palette__success-dark)",
"--uxdsl__button__contained-tone-success-selected-color": "var(--uxdsl__palette__success-contrast)",
"--uxdsl__button__contained-tone-info-selected-bg": "var(--uxdsl__palette__info-dark)",
"--uxdsl__button__contained-tone-info-selected-color": "var(--uxdsl__palette__info-contrast)",
"--uxdsl__button__contained-tone-warning-selected-bg": "var(--uxdsl__palette__warning-dark)",
"--uxdsl__button__contained-tone-warning-selected-color": "var(--uxdsl__palette__warning-contrast)",
"--uxdsl__button__contained-tone-error-selected-bg": "var(--uxdsl__palette__error-dark)",
"--uxdsl__button__contained-tone-error-selected-color": "var(--uxdsl__palette__error-contrast)",
"--uxdsl__button__contained-tone-dark-selected-bg": "var(--uxdsl__palette__dark-dark)",
"--uxdsl__button__contained-tone-dark-selected-color": "var(--uxdsl__palette__dark-contrast)",
"--uxdsl__button__contained-tone-neutral-selected-bg": "var(--uxdsl__palette__neutral-dark)",
"--uxdsl__button__contained-tone-neutral-selected-color": "var(--uxdsl__palette__neutral-contrast)",
"--uxdsl__button__contained-tone-light-selected-bg": "var(--uxdsl__palette__light-dark)",
"--uxdsl__button__contained-tone-light-selected-color": "var(--uxdsl__palette__light-contrast)"
}.action { @ds-button(outlined); }.action { padding: var(--uxdsl__surface__outlined-padding); border-radius: var(--uxdsl__surface__outlined-radius); background: var(--uxdsl__surface__outlined-bg); color: var(--uxdsl__surface__outlined-color); border: var(--uxdsl__surface__outlined-border); box-shadow: var(--uxdsl__surface__outlined-shadow); }
.action:hover { color: var(--uxdsl__button__outlined-hover-color); border: var(--uxdsl__button__outlined-hover-border); }
.action.is-selected, .action[aria-pressed="true"], .action[aria-selected="true"] { background: var(--uxdsl__button__outlined-selected-bg); color: var(--uxdsl__button__outlined-selected-color); border: var(--uxdsl__button__outlined-selected-border); }{
"--uxdsl__button__outlined-hover-color": "var(--uxdsl__button__tone-dark, var(--uxdsl__palette__primary-dark))",
"--uxdsl__button__outlined-hover-border": "1px solid var(--uxdsl__button__tone-dark, var(--uxdsl__palette__primary-dark))",
"--uxdsl__button__outlined-tone-primary-hover-color": "var(--uxdsl__palette__primary-dark)",
"--uxdsl__button__outlined-tone-primary-hover-border": "1px solid var(--uxdsl__palette__primary-dark)",
"--uxdsl__button__outlined-tone-secondary-hover-color": "var(--uxdsl__palette__secondary-dark)",
"--uxdsl__button__outlined-tone-secondary-hover-border": "1px solid var(--uxdsl__palette__secondary-dark)",
"--uxdsl__button__outlined-tone-surface-hover-color": "var(--uxdsl__palette__surface-dark)",
"--uxdsl__button__outlined-tone-surface-hover-border": "1px solid var(--uxdsl__palette__surface-dark)",
"--uxdsl__button__outlined-tone-tertiary-hover-color": "var(--uxdsl__palette__tertiary-dark)",
"--uxdsl__button__outlined-tone-tertiary-hover-border": "1px solid var(--uxdsl__palette__tertiary-dark)",
"--uxdsl__button__outlined-tone-success-hover-color": "var(--uxdsl__palette__success-dark)",
"--uxdsl__button__outlined-tone-success-hover-border": "1px solid var(--uxdsl__palette__success-dark)",
"--uxdsl__button__outlined-tone-info-hover-color": "var(--uxdsl__palette__info-dark)",
"--uxdsl__button__outlined-tone-info-hover-border": "1px solid var(--uxdsl__palette__info-dark)",
"--uxdsl__button__outlined-tone-warning-hover-color": "var(--uxdsl__palette__warning-dark)",
"--uxdsl__button__outlined-tone-warning-hover-border": "1px solid var(--uxdsl__palette__warning-dark)",
"--uxdsl__button__outlined-tone-error-hover-color": "var(--uxdsl__palette__error-dark)",
"--uxdsl__button__outlined-tone-error-hover-border": "1px solid var(--uxdsl__palette__error-dark)",
"--uxdsl__button__outlined-tone-dark-hover-color": "var(--uxdsl__palette__dark-dark)",
"--uxdsl__button__outlined-tone-dark-hover-border": "1px solid var(--uxdsl__palette__dark-dark)",
"--uxdsl__button__outlined-tone-neutral-hover-color": "var(--uxdsl__palette__neutral-dark)",
"--uxdsl__button__outlined-tone-neutral-hover-border": "1px solid var(--uxdsl__palette__neutral-dark)",
"--uxdsl__button__outlined-tone-light-hover-color": "var(--uxdsl__palette__light-dark)",
"--uxdsl__button__outlined-tone-light-hover-border": "1px solid var(--uxdsl__palette__light-dark)",
"--uxdsl__button__outlined-selected-bg": "var(--uxdsl__button__tone-main, var(--uxdsl__palette__primary-main))",
"--uxdsl__button__outlined-selected-color": "var(--uxdsl__button__tone-contrast, var(--uxdsl__palette__primary-contrast))",
"--uxdsl__button__outlined-selected-border": "1px solid var(--uxdsl__button__tone-main, var(--uxdsl__palette__primary-main))",
"--uxdsl__button__outlined-tone-primary-selected-bg": "var(--uxdsl__palette__primary-main)",
"--uxdsl__button__outlined-tone-primary-selected-color": "var(--uxdsl__palette__primary-contrast)",
"--uxdsl__button__outlined-tone-primary-selected-border": "1px solid var(--uxdsl__palette__primary-main)",
"--uxdsl__button__outlined-tone-secondary-selected-bg": "var(--uxdsl__palette__secondary-main)",
"--uxdsl__button__outlined-tone-secondary-selected-color": "var(--uxdsl__palette__secondary-contrast)",
"--uxdsl__button__outlined-tone-secondary-selected-border": "1px solid var(--uxdsl__palette__secondary-main)",
"--uxdsl__button__outlined-tone-surface-selected-bg": "var(--uxdsl__palette__surface-main)",
"--uxdsl__button__outlined-tone-surface-selected-color": "var(--uxdsl__palette__surface-contrast)",
"--uxdsl__button__outlined-tone-surface-selected-border": "1px solid var(--uxdsl__palette__surface-main)",
"--uxdsl__button__outlined-tone-tertiary-selected-bg": "var(--uxdsl__palette__tertiary-main)",
"--uxdsl__button__outlined-tone-tertiary-selected-color": "var(--uxdsl__palette__tertiary-contrast)",
"--uxdsl__button__outlined-tone-tertiary-selected-border": "1px solid var(--uxdsl__palette__tertiary-main)",
"--uxdsl__button__outlined-tone-success-selected-bg": "var(--uxdsl__palette__success-main)",
"--uxdsl__button__outlined-tone-success-selected-color": "var(--uxdsl__palette__success-contrast)",
"--uxdsl__button__outlined-tone-success-selected-border": "1px solid var(--uxdsl__palette__success-main)",
"--uxdsl__button__outlined-tone-info-selected-bg": "var(--uxdsl__palette__info-main)",
"--uxdsl__button__outlined-tone-info-selected-color": "var(--uxdsl__palette__info-contrast)",
"--uxdsl__button__outlined-tone-info-selected-border": "1px solid var(--uxdsl__palette__info-main)",
"--uxdsl__button__outlined-tone-warning-selected-bg": "var(--uxdsl__palette__warning-main)",
"--uxdsl__button__outlined-tone-warning-selected-color": "var(--uxdsl__palette__warning-contrast)",
"--uxdsl__button__outlined-tone-warning-selected-border": "1px solid var(--uxdsl__palette__warning-main)",
"--uxdsl__button__outlined-tone-error-selected-bg": "var(--uxdsl__palette__error-main)",
"--uxdsl__button__outlined-tone-error-selected-color": "var(--uxdsl__palette__error-contrast)",
"--uxdsl__button__outlined-tone-error-selected-border": "1px solid var(--uxdsl__palette__error-main)",
"--uxdsl__button__outlined-tone-dark-selected-bg": "var(--uxdsl__palette__dark-main)",
"--uxdsl__button__outlined-tone-dark-selected-color": "var(--uxdsl__palette__dark-contrast)",
"--uxdsl__button__outlined-tone-dark-selected-border": "1px solid var(--uxdsl__palette__dark-main)",
"--uxdsl__button__outlined-tone-neutral-selected-bg": "var(--uxdsl__palette__neutral-main)",
"--uxdsl__button__outlined-tone-neutral-selected-color": "var(--uxdsl__palette__neutral-contrast)",
"--uxdsl__button__outlined-tone-neutral-selected-border": "1px solid var(--uxdsl__palette__neutral-main)",
"--uxdsl__button__outlined-tone-light-selected-bg": "var(--uxdsl__palette__light-main)",
"--uxdsl__button__outlined-tone-light-selected-color": "var(--uxdsl__palette__light-contrast)",
"--uxdsl__button__outlined-tone-light-selected-border": "1px solid var(--uxdsl__palette__light-main)"
}.action { @ds-button(flat); }.action { padding: var(--uxdsl__surface__flat-padding); border-radius: var(--uxdsl__surface__flat-radius); background: var(--uxdsl__surface__flat-bg); color: var(--uxdsl__surface__flat-color); border: var(--uxdsl__surface__flat-border); box-shadow: var(--uxdsl__surface__flat-shadow); }
.action:hover { color: var(--uxdsl__button__flat-hover-color); }
.action.is-selected, .action[aria-pressed="true"], .action[aria-selected="true"] { color: var(--uxdsl__button__flat-selected-color); }{
"--uxdsl__button__flat-hover-color": "var(--uxdsl__button__tone-dark, var(--uxdsl__palette__primary-dark))",
"--uxdsl__button__flat-tone-primary-hover-color": "var(--uxdsl__palette__primary-dark)",
"--uxdsl__button__flat-tone-secondary-hover-color": "var(--uxdsl__palette__secondary-dark)",
"--uxdsl__button__flat-tone-surface-hover-color": "var(--uxdsl__palette__surface-dark)",
"--uxdsl__button__flat-tone-tertiary-hover-color": "var(--uxdsl__palette__tertiary-dark)",
"--uxdsl__button__flat-tone-success-hover-color": "var(--uxdsl__palette__success-dark)",
"--uxdsl__button__flat-tone-info-hover-color": "var(--uxdsl__palette__info-dark)",
"--uxdsl__button__flat-tone-warning-hover-color": "var(--uxdsl__palette__warning-dark)",
"--uxdsl__button__flat-tone-error-hover-color": "var(--uxdsl__palette__error-dark)",
"--uxdsl__button__flat-tone-dark-hover-color": "var(--uxdsl__palette__dark-dark)",
"--uxdsl__button__flat-tone-neutral-hover-color": "var(--uxdsl__palette__neutral-dark)",
"--uxdsl__button__flat-tone-light-hover-color": "var(--uxdsl__palette__light-dark)",
"--uxdsl__button__flat-selected-color": "var(--uxdsl__button__tone-dark, var(--uxdsl__palette__primary-dark))",
"--uxdsl__button__flat-tone-primary-selected-color": "var(--uxdsl__palette__primary-dark)",
"--uxdsl__button__flat-tone-secondary-selected-color": "var(--uxdsl__palette__secondary-dark)",
"--uxdsl__button__flat-tone-surface-selected-color": "var(--uxdsl__palette__surface-dark)",
"--uxdsl__button__flat-tone-tertiary-selected-color": "var(--uxdsl__palette__tertiary-dark)",
"--uxdsl__button__flat-tone-success-selected-color": "var(--uxdsl__palette__success-dark)",
"--uxdsl__button__flat-tone-info-selected-color": "var(--uxdsl__palette__info-dark)",
"--uxdsl__button__flat-tone-warning-selected-color": "var(--uxdsl__palette__warning-dark)",
"--uxdsl__button__flat-tone-error-selected-color": "var(--uxdsl__palette__error-dark)",
"--uxdsl__button__flat-tone-dark-selected-color": "var(--uxdsl__palette__dark-dark)",
"--uxdsl__button__flat-tone-neutral-selected-color": "var(--uxdsl__palette__neutral-dark)",
"--uxdsl__button__flat-tone-light-selected-color": "var(--uxdsl__palette__light-dark)"
}