Actionsstable.button
Button
Triggers an action, or navigates when given an href.
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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | optional | — | 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. |
icon | string | optional | — | Icon id from the sprite, shown before the label. |
iconOnly | boolean | optional | false | Square button showing only icon. Requires ariaLabel. |
ariaLabel | string | optional | — | Accessible name when the visible label is missing or ambiguous ("Weitere Aktionen für Lea Müller"). |
href | string | optional | — | Render an <a> that looks like a button (navigation, not actions). |
type | "button" | "submit" | "reset" | optional | "button" | Button type inside forms. |
value | string | optional | — | Form value, e.g. cancel / confirm inside <form method="dialog">. |
name | string | optional | — | Form field name when the button submits a value. |
disabled | boolean | optional | false | Not available now. Prefer explaining why nearby over silently disabling. |
busy | boolean | optional | false | Action in progress (aria-busy). |
formNoValidate | boolean | optional | false | Submit 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. |
behavior | string | optional | — | Behaviour hook (data-behavior), e.g. open-dialog, theme-toggle. |
action | string | optional | — | 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-]*$ |
actionId | string | optional | — | 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_-]+$ |
target | string | optional | — | Id of the element a behaviour acts on (data-target), e.g. the dialog to open. |
popoverTarget | string | optional | — | 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
| Attribute | Values | Default | Description |
|---|---|---|---|
data-variant | primary | ghost | danger | danger-ghost | (none = secondary) | Visual variant. |
data-size | sm | lg | (none = 44 px) | Height. |
data-icon-only | — | — | Square icon button. |
data-block | "" | mobile | — | Full width (always / only on phones). |
aria-label | text | — | Required for icon-only buttons. |
aria-busy | true | — | Shows the spinner, blocks clicks. |
data-behavior / data-target / popovertarget | see behaviours | — | Connect the button to dialogs, menus, theme toggle. |
data-action / data-id | action name / id | — | Trigger a declared screen action (see Screens). |
Parts
| Part | Description |
|---|---|
svg.icon | Optional leading icon from the sprite. |
States
| State | Description |
|---|---|
:hover / :active | Background shift / 1 px press. |
:focus-visible | Global 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 clickablediv. - Icon-only buttons need
aria-label; the icon itself isaria-hidden. - Minimum target 44 × 44 px (36 px only in desktop tables).
Guidance
danger for "Abbrechen".Related: Menu · Confirm dialog