Button
An independent React adapter built on Ark Factory. Button uses native semantics and keyboard behavior, so it does not require a Zag machine.
Design
Functional preview
Running with the independent Ark UI adapter and Zag behavior.
Styles
Anatomy
2 Icon (optional) — 20px frame/icon on sm and 24px on md/lg.
3 Label (
.ds-button__label) — typography and color via Component tokens.4 Root padding — sm 8×8px, md 10×12px, lg 12×16px.
5 Label padding — additional 4px on the Label Frame for optical balance.
Sizes
| Size | Height | Font | Padding | Touch target |
|---|---|---|---|---|
Small (--sm) | 32px (component.button.height.sm) | component.button.label.font-size.sm | 8×8px (component.button.padding-y.sm × padding-x.sm) | 32px — meets WCAG 2.5.8 min (24px) |
Medium (--md) | 40px (component.button.height.md) | component.button.label.font-size.md | 10×12px (component.button.padding-y.md × padding-x.md) | 40px |
Large (--lg) | 48px (component.button.height.lg) | component.button.label.font-size.lg | 12×16px (component.button.padding-y.lg × padding-x.lg) | 48px — meets AAA 44px target |
States
States are handled via CSS pseudo-classes (:hover, :active, :focus-visible, [disabled]), not modifier classes.
| State | CSS trigger | Visual change | Token |
|---|---|---|---|
| Default | — | Primary fill | component.button.bg.brand.default |
| Hover | :hover | Darker fill | component.button.bg.brand.hover |
| Pressed | :active | Darkest fill | component.button.bg.brand.pressed |
| Focus | :focus-visible | 2px ring with 2px offset and focus-state colors | component.button.bg.brand.focus, component.button.content.color.brand.focus, component.button.focus-ring.* |
| Disabled | [disabled] | Muted bg + content, no pointer events | component.button.bg.brand.disabled, component.button.content.color.brand.disabled |
With icon
Icon only
Loading
Full width
Token mapping
Tokens consumed by the Brand variant. Other variants follow the same pattern using their respective color groups.
| Property | Token (Component) | CSS variable |
|---|---|---|
| height (sm) | component.button.height.sm | --ds-button-height-sm |
| height (md) | component.button.height.md | --ds-button-height-md |
| height (lg) | component.button.height.lg | --ds-button-height-lg |
| min-width (sm/md/lg) | component.button.min-width.{sm,md,lg} | --ds-button-min-width-* |
| padding-x (sm/md/lg) | component.button.padding-x.{sm,md,lg} | --ds-button-padding-x-* |
| padding-y (sm/md/lg) | component.button.padding-y.{sm,md,lg} | --ds-button-padding-y-* |
| gap (sm/md/lg) | component.button.gap.{sm,md,lg} | --ds-button-gap-* |
| radius / border-width | component.button.radius.default, component.button.border-width.default | --ds-button-radius-default, --ds-button-border-width-default |
| bg por estilo/estado | component.button.bg.{brand,toned,outline,ghost,success,danger}.* | --ds-button-bg-* |
| border color Outline | component.button.border-color.outline.* | --ds-button-border-color-outline-* |
| text/icon color | component.button.content.color.{style}.{state} | --ds-button-content-color-* |
| label typography | component.button.label.{font-size,line-height,font-weight,letter-spacing}.* | --ds-button-label-* |
| icon frame/size | component.button.icon.size.{sm,md,lg}, component.button.icon.stroke-width.{sm,md,lg} | --ds-button-icon-* |
| icon-only | component.button.icon-only-{width,padding}.{sm,md,lg} | --ds-button-icon-only-* |
| focus ring | component.button.focus-ring.radius.default, component.focus-ring.{width,color.default,color.success,color.error} | --ds-focus-ring-* + --ds-button-focus-ring-radius-default |
Toned variant
--ds-toned-background-default, --ds-toned-background-hover, --ds-toned-background-active) instead of opaque fills. This differs from Subtle/Muted patterns which use opaque background tokens. The translucent approach lets the Toned button adapt naturally to any surface color beneath it while maintaining a consistent brand tint.
Figma
The Figma component exposes the properties below in the panel. Visibility booleans sit immediately above the slot they control.
| Property | Type | Default | Options |
|---|---|---|---|
| Style | Variant | Brand | Brand, Toned, Outline, Ghost, Success, Danger |
| Size | Variant | Medium | Small (32px), Medium (40px), Large (48px) |
| State | Variant | Default | Default, Hover, Pressed, Focused, Disabled |
| Icon Only | Variant | false | Default (with label), Icon Only (square) |
| Loading | Boolean | false | true, false |
| Show Left Icon | Boolean | false | Toggles visibility for the left icon frame |
| Left Icon | Instance swap | Placeholder | Sets the left icon component |
| Show Right Icon | Boolean | false | Toggles visibility for the right icon frame |
| Right Icon | Instance swap | Placeholder | Sets the right icon component |
Usage
When to use
<a>) instead. Don't use buttons for inline text actions or when the visual weight would compete with the page's primary action.
| Style | Use for | Emphasis |
|---|---|---|
| Brand | Primary action on the page. One per section, ideally one per page. | Highest |
| Toned | Secondary actions that need visibility but shouldn't compete with Brand. | Medium |
| Outline | Secondary or tertiary actions. Pairs well next to a Brand button. | Medium |
| Ghost | Tertiary actions, toolbar actions, or close/dismiss buttons. | Low |
| Success | Confirming a positive outcome: approve, complete, publish. | Contextual |
| Danger | Destructive actions: delete, remove, revoke. Always requires confirmation. | Contextual |
Best practices
Content guidelines
| Rule | Example |
|---|---|
| Use verb + noun for clarity | "Save changes", "Add item", "Delete project" — not just "Submit" or "OK" |
| Use sentence case | "Save changes" — not "Save Changes" or "SAVE CHANGES" |
| Keep labels short (1–3 words) | "Export CSV" — not "Click here to export your data as CSV" |
| Be specific about destructive actions | "Delete account" — not "Delete" or "Remove" |
| Avoid generic labels | "Confirm order" — not "Click here" or "Yes" |
Icon-only buttons need aria-label | aria-label="Close dialog" — describes the action, not the icon |
When to choose this output
Choose this adapter in projects that adopted Ark UI and Zag as their behavioral architecture. Do not import it into the shadcn/Base UI output.
Implementation
- Status
- Beta
- Distribution
- Source adapter
Adapter source
packages/react/src/ark/button.jsx
import { Button } from '@tis/react/ark/button'Accessibility
Keyboard interaction
| Key | Action |
|---|---|
Tab | Moves focus to the button (or past it if disabled) |
Enter | Activates the button |
Space | Activates the button |
Disabled buttons ([disabled]) are removed from tab order automatically by the browser. Loading buttons should also set aria-disabled="true" to prevent interaction while preserving tab order.
Accessibility
| WCAG criterion | Requirement | Status |
|---|---|---|
| 2.4.11 Focus Appearance (AA) | Focus ring 2px + 2px gap, contrast ≥ 3:1 against adjacent colors | ✓ |
| 2.5.8 Target Size min (AA) | Smallest size (32px) exceeds 24px minimum | ✓ |
| 2.5.5 Target Size (AAA) | Large size (48px) meets 44px touch target | ✓ |
| 1.4.3 Contrast (AA) | White text on Brand/Danger fill ≥ 4.5:1. Disabled exempt. | ✓ |
| 4.1.2 Name, Role, Value (A) | Use <button> element. Icon-only requires aria-label. | ✓ |
| 1.4.1 Use of Color (A) | Focused state has visible ring. Disabled has dimmed text + reduced opacity. | ✓ |
aria-label — required on icon-only buttons.aria-disabled="true" + aria-busy="true" — set on loading buttons.disabled — use the native attribute for truly disabled buttons (removed from tab order).
Output responsibility
Preserve the Ark/Zag semantics and validate Escape, outside interaction, ARIA relationships, and focus return.
Validation evidence
Covered by its independent Storybook output plus browser, responsive, keyboard, Axe, and bundle checks.