Surfaces

A container role composes padding, corners, colors, border and shadow from the theme; a component names the role.

Shared engine playground

Actual viewport: 0px. Resize the browser to test transitions. The preview uses the active theme and shared engine defaults. Edits below are scoped to this demo and do not save source JSON.

Surface presets

contained

A shared container treatment.

.card { @ds-surface(contained); }
Resolved preset references
{
  "--uxdsl__surface__contained-padding": "var(--uxdsl__density__2)",
  "--uxdsl__surface__contained-radius": "var(--uxdsl__radius__2)",
  "--uxdsl__surface__contained-bg": "var(--uxdsl__palette__surface-main)",
  "--uxdsl__surface__contained-color": "var(--uxdsl__palette__surface-contrast)",
  "--uxdsl__surface__contained-border": "1px solid var(--uxdsl__palette__surface-dark)",
  "--uxdsl__surface__contained-shadow": "var(--uxdsl__shadow__1)"
}

outlined

A shared container treatment.

.card { @ds-surface(outlined); }
Resolved preset references
{
  "--uxdsl__surface__outlined-padding": "var(--uxdsl__density__2)",
  "--uxdsl__surface__outlined-radius": "var(--uxdsl__radius__2)",
  "--uxdsl__surface__outlined-bg": "transparent",
  "--uxdsl__surface__outlined-color": "var(--uxdsl__palette__surface-contrast)",
  "--uxdsl__surface__outlined-border": "1px solid var(--uxdsl__palette__neutral-main)",
  "--uxdsl__surface__outlined-shadow": "none"
}

flat

A shared container treatment.

.card { @ds-surface(flat); }
Resolved preset references
{
  "--uxdsl__surface__flat-padding": "var(--uxdsl__density__2)",
  "--uxdsl__surface__flat-radius": "var(--uxdsl__radius__2)",
  "--uxdsl__surface__flat-bg": "transparent",
  "--uxdsl__surface__flat-color": "var(--uxdsl__palette__surface-contrast)",
  "--uxdsl__surface__flat-border": "none",
  "--uxdsl__surface__flat-shadow": "none"
}

Theme uxdsl.theme.json, merged over the base theme

{
  "surfaces": {
    "contained": {
      "shadow": "xs(shadow(1)) md(shadow(3))"
    }
  }
}

UXDSL you write example.uxdsl

.card { @ds-surface(contained); }
.notice { @ds-surface(outlined info 2); }

CSS you get compiled by uxdsl; :root shows only this token

:root { --uxdsl__surface__contained-shadow: var(--uxdsl__shadow__1); }
@media (min-width: 768px) {
  :root { --uxdsl__surface__contained-shadow: var(--uxdsl__shadow__3); }
}
.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);
}
.notice {
  padding: var(--uxdsl__density__2);
  border-radius: var(--uxdsl__radius__2);
  background: transparent;
  color: var(--uxdsl__palette__info-main);
  border: 1px solid var(--uxdsl__palette__info-main);
  box-shadow: var(--uxdsl__surface__outlined-shadow);
}

Rules

  • A surface role composes six fields — padding, radius, bg, color, border, shadow — from the other families. The base roles are contained, outlined, flat; a project adds its own in surfaces, inheriting contained’s fields.
  • @ds-surface(role [tone] [size] [radius(k)] [shadow(k)]): a tone (a Palette family with main, dark, contrast) swaps the colors; a size n picks density(n) and radius(n); radius() and shadow() override one field.
  • The directive is a direct child of the rule it styles (UXD_DIRECTIVE_CONTEXT otherwise) and is not itself responsive: put a responsive value on a property, or in the role’s field in the theme.
  • A declaration after the directive wins; that is the way to make one component an exception. Changing a field of a role changes every component that names it.
  • surface, light and dark are canvas colors: use them as a tone only on contained (see Accessibility).
.card { @ds-surface(contained); }
.notice { @ds-surface(outlined info 2); }
.hero { @ds-surface(contained primary shadow(3)); }
.card--flat {
  @ds-surface(contained);
  box-shadow: shadow(0);
}
.panel { @ds-surface(contained text); }  /* UXD_SURFACE_TONE: text is not a tone */
.panel { @ds-surface(glass); }  /* UXD_SURFACE_REFERENCE: no glass role in the theme */

The guide for coding agents is on For AI agents.

Many components, one role

Eighteen classes from one loop: every tile below is @ds-surface(role tone), so its padding, corners, border, shadow and colors come from the theme. Change the contained role in the theme and every contained tile follows; switch the site theme in the header and all of them do.

$roles: contained, outlined, flat;
$tones: primary, secondary, success, info, warning, error;

.surface-grid {
  display: grid;
  grid-template-columns: xs(repeat(2, 1fr)) md(repeat(3, 1fr)) xl(repeat(6, 1fr));
  gap: density(3);
}

@each $role in $roles {
  @each $tone in $tones {
    .surface-grid__tile--#{$role}-#{$tone} {
      @ds-surface(#{$role} #{$tone});
    }
  }
}

primary

contained

secondary

contained

success

contained

info

contained

warning

contained

error

contained

primary

outlined

secondary

outlined

success

outlined

info

outlined

warning

outlined

error

outlined

primary

flat

secondary

flat

success

flat

info

flat

warning

flat

error

flat