Button
Triggers an action, with variants for emphasis, a danger tone for destructive actions, three sizes plus icon-only, and a pending state.
Preview
Installation
pnpm add @syntara/react @syntara/tokensimport { Button } from '@syntara/react';Usage
import { Button } from '@syntara/react';
export function SaveButton() {
return <Button onPress={() => save()}>Save changes</Button>;
}Examples
Variants
Every variant, including the monochrome contrast action.
Tone
tone="danger" on primary, outline and ghost: one solid destructive action and two quieter ones. Disabled looks the same in every tone.
Contrast
The monochrome strong action: near-black in light schemes, near-white in dark, at the lg hero size.
Sizes
sm, md (the control height), lg (the hero size) and a square icon button, which requires an aria-label.
With icons
Icons in children are sized automatically; arrows flip in right-to-left layouts.
Pending
The spinner replaces the leading icon and the width never changes.
Accessibility
| Keys | Action |
|---|---|
| EnterorSpace | Activates the button. |
| Tab | Moves focus to the button, including while pending. |
- Icon-only buttons must have an aria-label; the type requires it for size="icon".
- While pending the button stays focusable, is aria-disabled and aria-busy, and screen readers hear the label again when the state changes.
- Focus ring: 2px focus.ring outline with a 2px offset, keyboard only.
- Every size keeps a target of at least 24×24px, including the link variant.
- tone="danger" is colour only. The label must say what is destroyed, e.g. "Delete account".
- The danger label on outline and ghost buttons is at least 4.5:1 on every opaque surface and on the hover face, for every tenant and 1,000 generated brands (test/button.test.tsx).
Guidelines
Do
- Use one primary button per view for the main action.
- Use a verb for the label: "Save changes", not "OK".
- Use isPending for async actions instead of disabling the button.
- Use variant="contrast" for strong actions when the page's one hero action already carries the brand colour.
- Use tone="danger" for actions that delete or can't be undone, and name the thing in the label: "Delete account".
- Use the solid danger button (primary) for the final confirming step. Use outline or ghost for destructive actions that sit in a row, a toolbar or a settings list.
- Inside dialogs, popovers and menus use primary or outline with tone="danger": both have an opaque face.
Don’t
- Don't use tone="danger" for anything that isn't destructive.
- Don't put a ghost button with tone="danger" directly on a glass overlay (dialog, popover, menu, sheet). Its face is transparent, and feedback colours aren't solved for contrast on glass.
- Don't use variant="danger" in new code. It is deprecated and will be removed in 1.0.0.
- Don't use the link variant for navigation; use Link, which renders an anchor.
- Don't put a contrast and a primary button side by side: that's two loud actions. Pair either with outline or ghost.
API reference
Button
variantprimarydefaultsecondaryoutlineghostlinkcontrastdangerdeprecated
Visual emphasis. Use one primary per view. contrast is the monochrome strong action (surface.inverse + text.inverse), for when brand colour should stay rare. For destructive actions set tone="danger". 'danger' is deprecated: it renders as before and logs one warning in development.
Deprecated:
variant="danger", since 0.2.0. It keeps working until 1.0.0. Usetone="danger"instead. Status belongs in tone, as in every other component; variant is only the visual emphasis.Migrate with
npx @syntara/codemods button-variant-danger-to-tone <path>. The decision is in RFC-001.toneneutraldefaultdanger
Feedback colour. danger marks a destructive action. It works with the primary (solid), outline and ghost variants and sets data-tone="danger". secondary, link and contrast ignore it: they render as neutral, with one warning in development.
sizesmmddefaultlgicon
md is the density's control height; sm is 8px shorter, lg 16px taller (the hero size). Height and corner radius match a field (TextField, Select, SearchField, Combobox) of the same size: the corner is radius.field plus half a space step, scaled 0.8 for sm and 1.25 for lg (round brands stay pills). Inline padding is ~1.25 × half the height. icon is a square button of the size's height and requires aria-label.
isPendingbooleanShows a spinner, sets aria-busy and aria-disabled, keeps focus and ignores presses. Width stays the same.
Default
falseisDisabledbooleanDisables the button.
Default
falseonPress(e: PressEvent) => voidCalled on mouse, touch, Enter or Space.
typebuttondefaultsubmitreset
Native button type for forms.
aria-labelstringAccessible name. Required when size="icon".
childrenRequiredReactNode | ((renderProps) => ReactNode)Label, optionally with Tabler icons (mark them aria-hidden).
classNamestring | ((renderProps) => string)Extra classes on the button, e.g. for full width.
Tokens
The semantic tokens this component reads, grouped by what they control. Swatches show this site’s theme; change a tenant’s brand and the component follows with no code change.
- Colour24
action.primary.bgaction.primary.borderaction.primary.fgaction.primary.hoveraction.primary.pressedaction.secondary.bgaction.secondary.fgaction.secondary.hoveraction.secondary.pressedborder.defaultborder.strongfeedback.danger.bgfeedback.danger.borderfeedback.danger.fgfeedback.danger.onSolidfeedback.danger.solidfocus.ringsurface.defaultsurface.inversetext.brandtext.defaulttext.disabledtext.inversetext.subtle- Type6
font.size.mdfont.size.smfont.tracking.mdfont.tracking.smfont.weight.mediumline-height.tight- Space and size6
control-heighticon-strokespace.1space.2space.4space.6- Shape3
radius.buttonradius.fieldradius.pill- Depth2
shadow.highlightshadow.raised- Motion5
motion.duration.fastmotion.duration.normalmotion.duration.springmotion.easingmotion.spring