Button

The one way the site asks the reader to act: book a conversation, start the health check, play a showcase.

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.ts

Open example: Button · Do: Primary with an arrow Use the full-page example to follow page links.

The page's main navigation action, with a direction cue.

Do: Secondary beside it

Rendered by button.ts

Open example: Button · Do: Secondary beside it Use the full-page example to follow page links.

The alternative link stays visible without competing.

Do: Native form action

Rendered by button.ts

Open example: Button · Do: Native form action Use the full-page example to follow page links.

The existing contact action renders a submit button, rather than a navigation link.

Do: Disabled native action

Rendered by button.ts

Open example: Button · Do: Disabled native action Use the full-page example to follow page links.

The existing native attribute prevents activation; no asynchronous controller is implied.

Do: Sending label and disabled action

Rendered by button.ts

Open example: Button · Do: Sending label and disabled action Use the full-page example to follow page links.

Authored busy presentation from the contact catalogue; the containing form still owns sending and recovery.

Do: Japanese form action

Rendered by button.ts

Open example: Button · Do: Japanese form action Use the full-page example to follow page links.

The same native action uses the existing Japanese contact label.

Do: Japanese sending presentation

Rendered by button.ts

Open example: Button · Do: Japanese sending presentation Use the full-page example to follow page links.

The existing Japanese sending label and native attributes express the busy presentation without inventing an async API.

Don't: A vague label

Rendered by button.ts

Open example: Button · Don't: A vague label Use the full-page example to follow page links.

The reader cannot tell what happens next.

Don't: Two primaries side by side

Rendered by button.ts

Open example: Button · Don't: Two primaries side by side Use the full-page example to follow page links.

Two equal actions cancel each other; one must be secondary.

Rules

RuleWhyCheck
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

PropTypeRequiredDefaultWhat it does
labelstringYesThe action, verb first (content token cta.* where one exists).
hrefstringNoWhere a link button goes; omitted for an on-page action (renders a <button>).
variantprimary | secondaryNo"primary"Primary for the main action, secondary for the alternative.
sizedefault | smallNo"default"Small only in the header and inside showcases.
arrowbooleanNofalseAdds the arrow icon after the label.
typebutton | submitNo"button"The type of an on-page button.
extraClassstringNoA placement class owned by the parent element (header-cta).
attrshtmlNoData 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

AspectValue
Idbutton
LayerAtoms
Categoryaction
OwnerSite system lead
Statusstable
Version1.0.0
Sourceapps/showcase/site/system/atoms/button.ts (private repository; the contract and the render function are this one module)
VersionDateChange
1.0.02026-10-26One 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

PartWhat it is
roota.btn (a link) or button.btn (an action on the page).
labelThe text: a verb first.
shuttleThe saffron bar on the left edge (an inset shadow) on the primary variant.
iconThe 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

VariantWhenWhy
primaryThe page's main action, once per view.Ink with the saffron shuttle: saffron is the colour of the one action.
secondaryThe alternative next to a primary (the free health check).An outline keeps it available without competing.
smallIn the header and inside showcases.Dense places need a 40 px control, still above the 24 px target minimum.

States

StateRendered or not applicable
defaultPrimary and secondary examples
hoverThe shuttle bar widens from 6 to 12 px and the arrow moves 3 px (hover-capable pointers only).
focus-visibleA 3 px focus ring in --focus, offset 3 px.
activeScales to 0.97 while pressed.
disabledOpacity 0.6 and a progress cursor while an action is running.
loadingShown as disabled with the label of the running action (the contact form's 'Sending…').
emptyNot applicable: A button always has a label; the builder rejects an empty one.
errorNot applicable: A button does not hold an error; the form field or callout beside it does.
currentNot applicable: Buttons do not mark the current page; navigation items do.
visitedNot applicable: Link buttons are calls to action and keep one colour so the action never looks done.
expandedNot applicable: Disclosure is the drawer's summary, not a button variant.

Tokens

PartToken
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-height48 px (40 px small)
root.radius--radius
root.transition--dur-slow --ease-standard (shuttle), --dur-fast (press)
--button-bar-space-3var(--space-3)
--button-gap-space-4var(--space-4)
--button-padding-space-4var(--space-4)
--button-padding-space-7var(--space-7)
--button-padding-space-6var(--space-6)
--button-border-radius-radiusvar(--radius)
--button-font-type-strongvar(--type-strong)
--button-background-inkvar(--ink)
--button-color-cottonvar(--cotton)
--button-box-shadow-saffronvar(--saffron)
--button-transition-dur-slowvar(--dur-slow)
--button-transition-ease-standardvar(--ease-standard)
--button-transition-dur-fastvar(--dur-fast)
--button-transition-dur-moderatevar(--dur-moderate)
--button-bar-space-5var(--space-5)
--button-box-shadow-rule-hairvar(--rule-hair)
--button-padding-left-space-7var(--space-7)
--button-font-type-small-strongvar(--type-small-strong)
--button-padding-right-space-6var(--space-6)
--button-padding-left-space-5var(--space-5)
--button-padding-left-space-6var(--space-6)
--button-padding-space-1var(--space-1)
--button-font-weight-weight-strongvar(--weight-strong)
--button-text-underline-offset-space-2var(--space-2)
--button-background-saffronvar(--saffron)
--button-color-on-saffronvar(--on-saffron)
--button-box-shadow-on-saffronvar(--on-saffron)
--button-color-field-fgvar(--field-fg)

Layout

AspectValue
min48 px tall, 44 px wide
max100% of its column
breakpointsRequired review at 390, 768, 1024 and 1440 px; execution is not implied by this declaration; under 600 px a button fills the row.
wrappingThe label wraps inside the button; it never truncates.
textExpansionJapanese and pseudo-localised labels up to 40% longer wrap onto a second line inside the same button.

Spacing and rhythm

AspectValue
internal--space-4 vertical, --space-7 right, the shuttle plus --space-6 left; gap --space-4 to the icon.
externalPlaced in a CtaPair (gap --space-5) or by its parent.
rhythmNot a section element.

Motion

AspectValue
movesThe shuttle bar width, the arrow and a press scale.
triggerHover (pointer devices) and press.
duration--dur-slow for the bar and arrow, --dur-fast for the press.
easing--ease-standard
interruptibleYes: CSS transitions reverse from where they are.
reducedKept: the movement is 3 to 6 px and under 250 ms, which reduced motion allows.
whyThe shuttle moving across the warp is the brand's one gesture: it shows the button is live.

Runtime and reuse

  1. Render-only element; mount any registered child enhancements separately. Preserve required Site CSS, fonts, responsive and locale rules from nativeElement.

Accessibility passport

AspectValue
rolelink (a) or button
nameThe visible label
keyboardEnter activates a link; Enter and Space activate a button.
focusIn document order; the 3 px --focus ring is always visible on keyboard focus.
screenReaderReads the label and the role; the arrow is hidden.
contrastCotton on ink 15.9:1; on-saffron on saffron 9.1:1; all states AA or better in light and dark.
target48 px tall (40 px small): above the WCAG 2.2 24 px minimum.

Content

SlotLengthVoice
label5 words (EN), 12 characters of Japanese per lineA 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

Usage

727 uses across the built pages (measured by npm run site:system).

Tests

AspectValue
unittests/unit/site-system.test.ts: renders every do and don't example through the registry and checks the classes it owns
visualNot yet: there are no screenshot baselines; the Library page is checked for sideways scroll at 1440 px by tests/site/system.spec.ts
a11ytests/site/system.spec.ts: axe (0 serious or critical) on the element's Library page in light and dark
parityscripts/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.

Open in Storybook

Start with a conversation about your system.

We read your system, show you what we found, and agree with your team what to fix first.