Button

Variants express importance, not decoration: one primary per view (placed last, at the end side), secondary for alternatives, ghost for low-emphasis actions, danger for destructive ones (which also need an undo or a confirm — see UX guide §5).

Labels are verbs that name the result ("Freigeben", "Prüfung abschliessen"), never "OK" or "Ja".

Set busy while the action runs: the button keeps its width, shows a spinner and ignores clicks. Icon-only buttons require ariaLabel.

Examples

Primary action

Data
render({
  "label": "Freigeben",
  "variant": "primary"
})
Markup
<button class="button" type="button" data-variant="primary">Freigeben</button>

With icon

Data
render({
  "label": "Training starten",
  "variant": "primary",
  "size": "lg",
  "icon": "play"
})
Markup
<button class="button" type="button" data-variant="primary" data-size="lg"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#play"/></svg>Training starten</button>

Icon only

Data
render({
  "icon": "more",
  "iconOnly": true,
  "variant": "ghost",
  "ariaLabel": "Weitere Aktionen für Lea Müller"
})
Markup
<button class="button" type="button" data-variant="ghost" data-icon-only aria-label="Weitere Aktionen für Lea Müller"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#more"/></svg></button>

Destructive

Data
render({
  "label": "Löschen",
  "variant": "danger"
})
Markup
<button class="button" type="button" data-variant="danger">Löschen</button>

Busy

Data
render({
  "label": "Speichern",
  "variant": "primary",
  "busy": true
})
Markup
<button class="button" type="button" data-variant="primary" aria-busy="true">Speichern</button>

Link styled as button

Data
render({
  "label": "Formular ansehen",
  "href": "#formular",
  "icon": "external-link"
})
Markup
<a class="button" href="#formular"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#external-link"/></svg>Formular ansehen</a>

Data contract

What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.

FieldTypeRequiredDefaultDescription
labelstringoptional—Visible text. Required unless iconOnly.
min 1 chars
variant"secondary" | "primary" | "ghost" | "danger" | "danger-ghost"optional"secondary"Visual weight / meaning.
size"sm" | "md" | "lg"optional"md"sm (36 px) only in dense desktop tables; lg (52 px) for the single main action of a screen.
iconstringoptional—Icon id from the sprite, shown before the label.
iconOnlybooleanoptionalfalseSquare button showing only icon. Requires ariaLabel.
ariaLabelstringoptional—Accessible name when the visible label is missing or ambiguous ("Weitere Aktionen für Lea Müller").
hrefstringoptional—Render an <a> that looks like a button (navigation, not actions).
type"button" | "submit" | "reset"optional"button"Button type inside forms.
valuestringoptional—Form value, e.g. cancel / confirm inside <form method="dialog">.
namestringoptional—Form field name when the button submits a value.
disabledbooleanoptionalfalseNot available now. Prefer explaining why nearby over silently disabling.
busybooleanoptionalfalseAction in progress (aria-busy).
formNoValidatebooleanoptionalfalseSubmit without native validation — use on "Abbrechen" inside <form method="dialog"> so required fields do not block closing.
block"always" | "mobile"optional—Full width always, or only below 768 px.
behaviorstringoptional—Behaviour hook (data-behavior), e.g. open-dialog, theme-toggle.
actionstringoptional—Screen action name (data-action). Declared in the actions of the screen with a handler and a risk class; the engine adds undo or confirm accordingly.
pattern ^[a-z][a-zA-Z0-9-]*$
actionIdstringoptional—Id of the thing the action applies to (data-id), when the button is not inside an element that has one.
pattern ^[A-Za-z0-9_-]+$
targetstringoptional—Id of the element a behaviour acts on (data-target), e.g. the dialog to open.
popoverTargetstringoptional—Id of a popover menu this button toggles (popovertarget).
JSON Schema
{
  "type": "object",
  "additionalProperties": false,
  "required": [],
  "properties": {
    "label": {
      "type": "string",
      "minLength": 1,
      "description": "Visible text. Required unless `iconOnly`."
    },
    "variant": {
      "type": "string",
      "enum": [
        "secondary",
        "primary",
        "ghost",
        "danger",
        "danger-ghost"
      ],
      "default": "secondary",
      "description": "Visual weight / meaning."
    },
    "size": {
      "type": "string",
      "enum": [
        "sm",
        "md",
        "lg"
      ],
      "default": "md",
      "description": "`sm` (36 px) only in dense desktop tables; `lg` (52 px) for the single main action of a screen."
    },
    "icon": {
      "type": "string",
      "description": "Icon id from the sprite, shown before the label."
    },
    "iconOnly": {
      "type": "boolean",
      "default": false,
      "description": "Square button showing only `icon`. Requires `ariaLabel`."
    },
    "ariaLabel": {
      "type": "string",
      "description": "Accessible name when the visible label is missing or ambiguous (\"Weitere Aktionen für Lea Müller\")."
    },
    "href": {
      "type": "string",
      "description": "Render an `<a>` that looks like a button (navigation, not actions)."
    },
    "type": {
      "type": "string",
      "enum": [
        "button",
        "submit",
        "reset"
      ],
      "default": "button",
      "description": "Button type inside forms."
    },
    "value": {
      "type": "string",
      "description": "Form value, e.g. `cancel` / `confirm` inside `<form method=\"dialog\">`."
    },
    "name": {
      "type": "string",
      "description": "Form field name when the button submits a value."
    },
    "disabled": {
      "type": "boolean",
      "default": false,
      "description": "Not available now. Prefer explaining why nearby over silently disabling."
    },
    "busy": {
      "type": "boolean",
      "default": false,
      "description": "Action in progress (`aria-busy`)."
    },
    "formNoValidate": {
      "type": "boolean",
      "default": false,
      "description": "Submit without native validation — use on \"Abbrechen\" inside `<form method=\"dialog\">` so required fields do not block closing."
    },
    "block": {
      "type": "string",
      "enum": [
        "always",
        "mobile"
      ],
      "description": "Full width always, or only below 768 px."
    },
    "behavior": {
      "type": "string",
      "description": "Behaviour hook (`data-behavior`), e.g. `open-dialog`, `theme-toggle`."
    },
    "action": {
      "type": "string",
      "pattern": "^[a-z][a-zA-Z0-9-]*$",
      "description": "Screen action name (`data-action`). Declared in the `actions` of the screen with a handler and a risk class; the engine adds undo or confirm accordingly."
    },
    "actionId": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]+$",
      "description": "Id of the thing the action applies to (`data-id`), when the button is not inside an element that has one."
    },
    "target": {
      "type": "string",
      "description": "Id of the element a behaviour acts on (`data-target`), e.g. the dialog to open."
    },
    "popoverTarget": {
      "type": "string",
      "description": "Id of a popover menu this button toggles (`popovertarget`)."
    }
  }
}

Markup & states

Root: button | a.button — usable without render() by writing the markup directly.

Attributes

AttributeValuesDefaultDescription
data-variantprimary | ghost | danger | danger-ghost(none = secondary)Visual variant.
data-sizesm | lg(none = 44 px)Height.
data-icon-only——Square icon button.
data-block"" | mobile—Full width (always / only on phones).
aria-labeltext—Required for icon-only buttons.
aria-busytrue—Shows the spinner, blocks clicks.
data-behavior / data-target / popovertargetsee behaviours—Connect the button to dialogs, menus, theme toggle.
data-action / data-idaction name / id—Trigger a declared screen action (see Screens).

Parts

PartDescription
svg.iconOptional leading icon from the sprite.

States

StateDescription
:hover / :activeBackground shift / 1 px press.
:focus-visibleGlobal focus ring.
:disabled, [aria-disabled="true"]Muted colours, not-allowed cursor.
[aria-busy="true"]A spinner replaces the leading icon (width kept); the label stays; clicks blocked. Without a leading icon the spinner is added before the label.

Accessibility

  • Native <button> (or <a href> for navigation) — never a clickable div.
  • Icon-only buttons need aria-label; the icon itself is aria-hidden.
  • Minimum target 44 × 44 px (36 px only in desktop tables).

Guidance

DoOne primary per view; put it last.
DoName the outcome: "Prüfung abschliessen" rather than "Weiter".
Don'tNo more than 3 buttons in a row — move the rest into a menu.
Don'tDon't use danger for "Abbrechen".

Related: Menu · Confirm dialog