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 primary per screen or dialog — the accent marks the single main action.
  • default for the secondary action next to a primary (dialog footer, form bar).
  • ghost for quiet inline actions inside rows, cards and dense toolbars.
  • danger only 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 of loading (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 and className="w-full" on phone CTAs.
  • Footer floor (apps/web/src/ds/responsive.css) — every other .acrm-btn in a dialog footer (.acrm-dialog-foot, and the mail footer .mc-foot) gets min-height 44px 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), padding 0 var(--sp-4), gap --sp-3, radius --radius-sm, 1px border, label 500 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 kbd hint (.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, sets disabled and aria-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> renders type="button" by default, like IconButton. 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">.
  • ref is 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 primary and one default ("Save" + "Cancel").
  • Put the primary action last in a footer, on the right; secondary to its left.
  • Use loading for 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 kbd where the shortcut actually works ("Cancel Esc", "Send ⌘↵").

Don't

  • Two primaries side by side ("Save" and "Send" both blue) — pick one.
  • A page-specific .btn override or a bigger custom button for login or marketing.
  • danger for a harmless action ("Close") or a non-destructive negative ("Decline").
  • loading on "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".
  • Icon button — icon-only quiet control for bars and rows.
  • Text field › Input with icon & kbd — the same .acrm-kbd chip inside a field.
  • Loader — the loading glyph loading stacks over the label.
  • Section top bar — the bar that hosts the one primary action of a section.

Source: docs/button/button.usage.md · GitLab