Button
A React source recipe with Base UI behavior and the public TIS Button classes and tokens.
Design
Functional preview
Running with the React source recipe distributed through shadcn and based on Base UI.
Open the React · shadcn/Base UI playground
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 |
React composition
import { Button } from "@/components/ui/button"
<Button type="submit">Salvar alterações</Button>Implementation
- Status
- Beta
- Distribution
- Source via shadcn
Installation
Configure the namespace once in the React integration guide.
npx shadcn@latest add @tis/buttonpnpm dlx shadcn@latest add @tis/buttonyarn dlx shadcn@latest add @tis/buttonbunx --bun shadcn@latest add @tis/buttonPublic contract
- Registry item
@tis/button- Provider
- Base UI
- Distribution
- Source copied into the application
- Status
- Beta
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 Base UI semantics and test real content, focus visibility, and accessible names in the consuming application.
Validation evidence
Covered by its independent Storybook output plus browser, responsive, keyboard, Axe, and bundle checks.