<!-- https://getkonjo.com/design/patterns/buttons · source: docs/design/patterns/buttons.md -->

# Buttons

Primary, secondary, link — and the one-filled-primary budget.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine apply here unless this chapter contradicts them.

> **Kata (2026-09-29).** Red is the action colour and buttons are pills
> ([interview](../2026-09-29-kata-founder-interview.md), V4). This table is the Kata version;
> [`<Button>`](../../../src/components/ui/Button.tsx) implements it.

| Role | Fill | Height | Radius | Label |
|---|---|---|---|---|
| Primary | `brand.red`, label `brand.onRed` (5.01:1, both themes). Studio too | 48 | `radius.pill` | `rowTitle` 16/600 |
| Primary inside a hero block | `brand.red` | 48 | `radius.pill` | `label` 13/700, caps (hero voice) |
| Secondary | none, 1px `border.control` in ink | 44 | `radius.pill` | `label` 13/700, `text.primary` |
| **Destructive** | the secondary style — an ink outline, **never red** — with a plain verb ("Delete event") behind the [serious-action](destructive.md) confirmation | 44 | `radius.pill` | `label` 13/700, `text.primary` |
| Text link | none | 44 target — pad 12 vertical + `hitSlop` when the label is short | — | `label` 13/700, **`accent.red`** |
| Chip | `surface.card` + `border.control`; **selected fills `brand.red` on `surface.card`**, label `brand.onRed` | 32 | `radius.pill` | `label` 13/700 |

**`label` 13/700 is for chips, secondary buttons and links — not for a 48pt primary.** A
48pt button carrying 13pt type is [anti-pattern #13](../../../.claude/skills/design/references/anti-patterns.md),
observed in Mindbody's "BOOK NOW": target size and label size decided independently. A
full-width primary takes `rowTitle` 16/600.

**Every button is a pill**, full-width primaries included. The August rule was the opposite (a
full-width pill "reads as a toy"); the founder chose pill buttons with soft cards in Kata V4,
over square and fully rounded alternatives, and the reason is recorded there.

**Red on a button means "tap here" and nothing else.** That is why a destructive action is an
ink outline: a red Delete would read as the thing to press. Errors use `feedback.errorText`, a
different red, and always with an icon and words.

**"Ink" as a text colour means `text.primary`, not `brand.ink`.** Ink is for section labels,
eyebrows and arrows (links are red now), and that is right — but `brand.ink` is theme-invariant `#111111`,
which is **1.021:1** on the dark page. It is the same trap the hero law names, and it applies
to a link label exactly as much as to a surface. `brand.ink` is a *fill* on a light surface,
or the hero surface itself. Text that must survive both themes uses `text.primary`.

**Loading state.** A button in flight keeps its fill, swaps its label ("Submitting…"), shows
a 16pt spinner to the **left** of the label at `spacing.s8`, and ignores taps. It is never
greyed — there is no legal disabled text colour in this system.

Heights come from `controlHeight`, not from a spacing token: `primary` 48, `control` 44
(secondary and inputs), `chip` 32, `row` 64. A spacing token doing a sizing job is how 48
ends up written as `spacing.s48`.
