Button
The single button component of the design system. React <Button> (packages/ui/src/components/Button.tsx) emits the CSS primitive .acrm-btn (apps/web/src/ds.css), so React and CSS-only screens share one definition (RULES.md §5).
When to use
- Any clickable action with a text label: save, cancel, create, send, delete.
- One
primaryper screen or dialog — the accent marks the single main action. defaultfor the secondary action next to a primary (dialog footer, form bar).ghostfor quiet inline actions inside rows, cards and dense toolbars.dangeronly for destructive, irreversible actions (delete, remove access).
When not to use
- Icon-only actions in top bars, headers and rows — use Icon button.
- Navigation to another page — use a link or a nav item.
- Toggling between views — use Segmented or Tabs.
- Loading more rows in a long list — hide the button and show
<ListLoader>instead ofloading(AD P93). - A taller "hero" button on desktop — the primitive is fixed at
--control-h. The only taller button is the phone touch target below, never a page-specific size.
Touch target on phone
Below 720px a finger needs 44px (calc(var(--sp-7) + var(--sp-1)) = 40 + 4). Two rules, each with one role (acrm !4007, 27 Sep 2026):
<Button touch>(.acrm-btn.touch,apps/web/src/ds/button.css) — the ONE large action of a phone footer or bottom sheet ("Done", "Create", "Select record"): 44px high and full width of its parent. Desktop unchanged. Replaces scoped height copies andclassName="w-full"on phone CTAs.- Footer floor (
apps/web/src/ds/responsive.css) — every other.acrm-btnin a dialog footer (.acrm-dialog-foot, and the mail footer.mc-foot) getsmin-height44px on phone, width unchanged, so "Cancel" and "Create contact" can stay side by side.
Don't add scoped height: 44px or font-size bumps per screen; pick touch or rely on the footer floor.
Anatomy
- Container:
inline-flex, height--control-h(30px), padding0 var(--sp-4), gap--sp-3, radius--radius-sm, 1px border, label500 13px var(--font-sans). - Label: sentence case, a verb, no trailing period.
- Optional icon (
<Icon>) before or after the label — 16px inside the button. - Optional
kbdhint (.acrm-kbd) rendered after the label,aria-hidden. - Variants:
primary(--accent, border--accent-press,--fg-on-accent),default(--surface-100, border--border-strong,--shadow-xs),ghost(transparent, hover--hover-surface),danger(--critical, hover--critical-ink). - States:
disabled(opacity 0.5, no pointer events),loading(<Loader>stacked over a transparent label; the button keeps its width, setsdisabledandaria-busy).
Keyboard hint
The shortcut chip .acrm-kbd is a slot of Button (kbd prop), not a component of its own (review ACRM-76). Inline chip, height 19px, padding 0 5px, 1px border --border-strong, radius --radius-xs; text 500 11px var(--font-sans), colour --fg-secondary. On primary and danger the chip switches to --fg-on-accent alpha border and ink (color-mix) so it stays visible on the accent fill. It is aria-hidden — the label carries the accessible name. Show it only where the shortcut is wired on the current screen ("Cancel Esc", "Send ⌘↵"), one hint per button, after the label, with the app's key glyphs ("Esc", "⌘↵", "↵") — never "Ctrl + Enter" in words. Never on Icon button (no label to sit next to) and never as the only label of an action. The same chip inside a field is Text field › Input with icon & kbd.
Dialog footers use this slot exclusively — the last private copy, Task dialog's <span className="tk-kb">, was migrated onto <Button kbd> in acrm !3997 (.tk-kb deleted); no screen should hand-roll its own footer shortcut chip any more. On phones (max-width: 720px, apps/web/src/ds/button.css) .acrm-kbd is hidden inside every .acrm-btn — nobody has a keyboard there and the chip only eats footer width (AD 27 Sep 2026, acrm !3997).
Default type and ref (acrm !4067, 28 Sep 2026)
<Button>renderstype="button"by default, likeIconButton. Inside a<form>a bare<button>submits the form — converting<button type="button">to<Button>used to change behaviour silently (the AI composer "/" submitted its form). A submit is always explicit:<Button type="submit">.refis part of the type (React 19 passes it through to<button>) — use it as a Popover anchor or focus target instead of wrapping the button.
Tone muted (2026-09-28, acrm !4088)
<Button variant="ghost" tone="muted"> — a quiet secondary action in a toolbar or sheet that should not pull the eye („Vymazat filtry", „Vymazat vše"): text and glyph --fg-secondary, --fg on hover. Only with ghost. Not for primary or destructive actions.
Best practices
Do
- One accent per screen: a dialog footer has one
primaryand onedefault("Save" + "Cancel"). - Put the primary action last in a footer, on the right; secondary to its left.
- Use
loadingfor the action the user just triggered: "Save" stays the same width while the loader runs. - Lead with an icon only when it carries meaning ("+ New deal"); put the icon before the label.
- Show the shortcut with
kbdwhere the shortcut actually works ("Cancel Esc", "Send ⌘↵").
Don't
- Two primaries side by side ("Save" and "Send" both blue) — pick one.
- A page-specific
.btnoverride or a bigger custom button for login or marketing. dangerfor a harmless action ("Close") or a non-destructive negative ("Decline").loadingon "Load more" in a list — swap the button for<ListLoader>.- Label plus explanatory sentence inside the button — keep the label to a verb phrase.
Content
- Sentence case, verb first: "Save changes", "New deal", "Remove".
- No trailing period, no exclamation mark.
- Keep labels short (1–3 words); the button never wraps (
white-space: nowrap). - Destructive labels say what is removed: "Delete contact", not "OK".
Related
- Icon button — icon-only quiet control for bars and rows.
- Text field › Input with icon & kbd — the same
.acrm-kbdchip inside a field. - Loader — the loading glyph
loadingstacks over the label. - Section top bar — the bar that hosts the one primary action of a section.
Source: docs/button/button.usage.md · GitLab