Atom · stable · v1.0.0 · owner: Site system lead · contract 19 of 20 points (open: figma)
Do and don't
Do: Primary with an arrow
Rendered by button.tsOpen example: Button · Do: Primary with an arrow Use the full-page example to follow page links.
Do: Secondary beside it
Rendered by button.tsOpen example: Button · Do: Secondary beside it Use the full-page example to follow page links.
Do: Native form action
Rendered by button.tsOpen example: Button · Do: Native form action Use the full-page example to follow page links.
Do: Disabled native action
Rendered by button.tsOpen example: Button · Do: Disabled native action Use the full-page example to follow page links.
Do: Sending label and disabled action
Rendered by button.tsOpen example: Button · Do: Sending label and disabled action Use the full-page example to follow page links.
Do: Japanese form action
Rendered by button.tsOpen example: Button · Do: Japanese form action Use the full-page example to follow page links.
Do: Japanese sending presentation
Rendered by button.tsOpen example: Button · Do: Japanese sending presentation Use the full-page example to follow page links.
Don't: A vague label
Rendered by button.tsOpen example: Button · Don't: A vague label Use the full-page example to follow page links.
Don't: Two primaries side by side
Rendered by button.tsOpen example: Button · Don't: Two primaries side by side Use the full-page example to follow page links.
Rules
| Rule | Why | Check |
|---|---|---|
| One primary button per view. | Saffron marks the one action; two would mean none. | Automated: scripts/site/system/compose.ts (one primary per section) |
| The label starts with a verb and says what happens. | People decide in the time it takes to read the first word. | Review: Content owner (Site system lead) at review |
| Navigation is an <a>; an action on the page is a <button>. | Assistive technology announces the role, so the role must match what happens. | Automated: tests/unit/site-system.test.ts |
| On a dye or ink field the primary button is saffron. | Saffron is the action colour on every ground, so the action looks the same wherever it sits. | Automated: brand gate (core/button.css) |
API
| Prop | Type | Required | Default | What it does |
|---|---|---|---|---|
label | string | Yes | The action, verb first (content token cta.* where one exists). | |
href | string | No | Where a link button goes; omitted for an on-page action (renders a <button>). | |
variant | primary | secondary | No | "primary" | Primary for the main action, secondary for the alternative. |
size | default | small | No | "default" | Small only in the header and inside showcases. |
arrow | boolean | No | false | Adds the arrow icon after the label. |
type | button | submit | No | "button" | The type of an on-page button. |
extraClass | string | No | A placement class owned by the parent element (header-cta). | |
attrs | html | No | Data or ARIA attributes the parent organism needs (data-action, aria-pressed). |
Contract details
The full contract: identity, anatomy, variants, states, tokens, layout, motion, accessibility, content, tests
Identity
| Aspect | Value |
|---|---|
| Id | button |
| Layer | Atoms |
| Category | action |
| Owner | Site system lead |
| Status | stable |
| Version | 1.0.0 |
| Source | apps/showcase/site/system/atoms/button.ts (private repository; the contract and the render function are this one module) |
| Version | Date | Change |
|---|---|---|
| 1.0.0 | 2026-10-26 | One button for the whole site: primary (ink with the saffron shuttle bar, saffron on a field), secondary (outlined) and small. |
Use when
- The reader takes the page's main action. Why: One strong, consistent shape makes the next step obvious on every page.
- A control changes something on the page (play, copy, submit). Why: A real button gives keyboard and screen-reader behaviour for free.
Do not use when
- The action only moves to another page inside running text. Why: A text link reads as part of the sentence; a button would break the line.
- More than one primary button in one view. Why: Two equal shouts mean neither is the main action.
Anatomy
| Part | What it is |
|---|---|
| root | a.btn (a link) or button.btn (an action on the page). |
| label | The text: a verb first. |
| shuttle | The saffron bar on the left edge (an inset shadow) on the primary variant. |
| icon | The optional arrow (Icon atom). |
Made of: icon, tokens:colour, tokens:type, tokens:space, tokens:motion.
Used in: cta-pair, playback-controls, atomic-guide, contact-form, first-90, header, health-check, survey.
Variants
| Variant | When | Why |
|---|---|---|
| primary | The page's main action, once per view. | Ink with the saffron shuttle: saffron is the colour of the one action. |
| secondary | The alternative next to a primary (the free health check). | An outline keeps it available without competing. |
| small | In the header and inside showcases. | Dense places need a 40 px control, still above the 24 px target minimum. |
States
| State | Rendered or not applicable |
|---|---|
| default | Primary and secondary examples |
| hover | The shuttle bar widens from 6 to 12 px and the arrow moves 3 px (hover-capable pointers only). |
| focus-visible | A 3 px focus ring in --focus, offset 3 px. |
| active | Scales to 0.97 while pressed. |
| disabled | Opacity 0.6 and a progress cursor while an action is running. |
| loading | Shown as disabled with the label of the running action (the contact form's 'Sending…'). |
| empty | Not applicable: A button always has a label; the builder rejects an empty one. |
| error | Not applicable: A button does not hold an error; the form field or callout beside it does. |
| current | Not applicable: Buttons do not mark the current page; navigation items do. |
| visited | Not applicable: Link buttons are calls to action and keep one colour so the action never looks done. |
| expanded | Not applicable: Disclosure is the drawer's summary, not a button variant. |
Tokens
| Part | Token |
|---|---|
| root.background | --ink (primary), transparent (secondary), --saffron on dye or ink fields |
| root.color | --cotton; --on-saffron on fields |
| shuttle.color | --saffron; --on-saffron on fields |
| root.font | --type-strong, semi-expanded |
| root.min-height | 48 px (40 px small) |
| root.radius | --radius |
| root.transition | --dur-slow --ease-standard (shuttle), --dur-fast (press) |
| --button-bar-space-3 | var(--space-3) |
| --button-gap-space-4 | var(--space-4) |
| --button-padding-space-4 | var(--space-4) |
| --button-padding-space-7 | var(--space-7) |
| --button-padding-space-6 | var(--space-6) |
| --button-border-radius-radius | var(--radius) |
| --button-font-type-strong | var(--type-strong) |
| --button-background-ink | var(--ink) |
| --button-color-cotton | var(--cotton) |
| --button-box-shadow-saffron | var(--saffron) |
| --button-transition-dur-slow | var(--dur-slow) |
| --button-transition-ease-standard | var(--ease-standard) |
| --button-transition-dur-fast | var(--dur-fast) |
| --button-transition-dur-moderate | var(--dur-moderate) |
| --button-bar-space-5 | var(--space-5) |
| --button-box-shadow-rule-hair | var(--rule-hair) |
| --button-padding-left-space-7 | var(--space-7) |
| --button-font-type-small-strong | var(--type-small-strong) |
| --button-padding-right-space-6 | var(--space-6) |
| --button-padding-left-space-5 | var(--space-5) |
| --button-padding-left-space-6 | var(--space-6) |
| --button-padding-space-1 | var(--space-1) |
| --button-font-weight-weight-strong | var(--weight-strong) |
| --button-text-underline-offset-space-2 | var(--space-2) |
| --button-background-saffron | var(--saffron) |
| --button-color-on-saffron | var(--on-saffron) |
| --button-box-shadow-on-saffron | var(--on-saffron) |
| --button-color-field-fg | var(--field-fg) |
Layout
| Aspect | Value |
|---|---|
| min | 48 px tall, 44 px wide |
| max | 100% of its column |
| breakpoints | Required review at 390, 768, 1024 and 1440 px; execution is not implied by this declaration; under 600 px a button fills the row. |
| wrapping | The label wraps inside the button; it never truncates. |
| textExpansion | Japanese and pseudo-localised labels up to 40% longer wrap onto a second line inside the same button. |
Spacing and rhythm
| Aspect | Value |
|---|---|
| internal | --space-4 vertical, --space-7 right, the shuttle plus --space-6 left; gap --space-4 to the icon. |
| external | Placed in a CtaPair (gap --space-5) or by its parent. |
| rhythm | Not a section element. |
Motion
| Aspect | Value |
|---|---|
| moves | The shuttle bar width, the arrow and a press scale. |
| trigger | Hover (pointer devices) and press. |
| duration | --dur-slow for the bar and arrow, --dur-fast for the press. |
| easing | --ease-standard |
| interruptible | Yes: CSS transitions reverse from where they are. |
| reduced | Kept: the movement is 3 to 6 px and under 250 ms, which reduced motion allows. |
| why | The shuttle moving across the warp is the brand's one gesture: it shows the button is live. |
Runtime and reuse
Render-only element; mount any registered child enhancements separately. Preserve required Site CSS, fonts, responsive and locale rules from nativeElement.
Accessibility passport
| Aspect | Value |
|---|---|
| role | link (a) or button |
| name | The visible label |
| keyboard | Enter activates a link; Enter and Space activate a button. |
| focus | In document order; the 3 px --focus ring is always visible on keyboard focus. |
| screenReader | Reads the label and the role; the arrow is hidden. |
| contrast | Cotton on ink 15.9:1; on-saffron on saffron 9.1:1; all states AA or better in light and dark. |
| target | 48 px tall (40 px small): above the WCAG 2.2 24 px minimum. |
Content
| Slot | Length | Voice |
|---|---|---|
| label | 5 words (EN), 12 characters of Japanese per line | A verb first, what happens next, no 'click here' |
Good: “Book a conversation” · “Start with a free health check” · “相談する”
Bad: “Submit” · “Click here” · “Learn more about our innovative platform”
Japanese: CTA labels come from content tokens (cta.primary, cta.secondary) with reviewed Japanese.
Icons and illustrations
-
icon:arrow-right
Decisions and exceptions
- D-SITE-001: Loom Site is the site's own design system, governed by Loom
- D-SITE-004: Colour as structure, marks as state
Usage
727 uses across the built pages (measured by npm run site:system).
Tests
| Aspect | Value |
|---|---|
| unit | tests/unit/site-system.test.ts: renders every do and don't example through the registry and checks the classes it owns |
| visual | Not yet: there are no screenshot baselines; the Library page is checked for sideways scroll at 1440 px by tests/site/system.spec.ts |
| a11y | tests/site/system.spec.ts: axe (0 serious or critical) on the element's Library page in light and dark |
| parity | scripts/site/system/check.ts: Library examples are rendered by the same render function the site imports |
Figma
not-yet: The Loom Site Figma library is the last stage of the plan; this element is not mapped yet.