UX-DSL
XS0px

Buttons

Shared action roles and interaction states, configured in the theme.

Shared action roles, built on Surfaces

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.

Define the role in JSON

{
  "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. */

What the component keeps

.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.

Tones, sizes and states

.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.

Legacy @theme and one shared engine

@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.

Interactive Demo

Shared engine playground

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); }
CSS from the shared engine
.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); }
CSS from the shared engine
.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); }
CSS from the shared engine
.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)"
}

AI implementation guide

How an AI agent should use Buttons

Responsibility: preserve shared action roles and their visual states. Preserve intent, not just the current computed value.

  • Inspect buttons, surfaces, Palette, Density, Radii, Borders, Shadows, breakpoints and legacy imports before choosing a role.
  • Reuse a configured Button role. Do not replace its directive with currently resolved colors, padding or state styles.
  • Choose semantic HTML and implement activation, disabled and toggle behavior independently of styling.
  • Inspect inherited base and state fields. Define a custom role in JSON before referencing it. Explicit base fields override the selected Surface composition.
  • Use a tone or numeric size only for intentional overrides. Check Palette variants and both Density and Radius keys. Never infer pixels from a token number.
  • Change shared definitions only when their consumers should change. Select another role or use subsequent CSS for an isolated exception.
  • Keep responsive progressions in the theme and preserve their token dependencies. Include a base value and verify persistence between thresholds.
  • Update existing values through managed theme CSS; regenerate component CSS for structural changes to roles or state fields.
  • Verify hover, active, selected, disabled, keyboard focus, contrast, wrapping and breakpoint boundaries. Check other consumers of shared dependencies.

Decision rule: Choose the configured action role in the component. Maintain shared visual behavior in the theme; keep interaction semantics in HTML and application logic.

Agent reasoning example

For “increase the selected checkout action depth on desktop,” inspect the checkout role, change its selected shadow progression and verify all checkout consumers around each threshold. Keep their Button references. For one exceptional action, use a suitable existing role or intentional local CSS.