Button
Examples
Variants (Default tone)
Negative tone
Static tones
Use on fixed light/dark surfaces where theme semantic colors should not shift. Demo panels use those surfaces (not the page chrome), so static tones stay readable in both light and dark docs mode.
Sizes
Sm 28px · Md 36px (default) · Lg 40px — matching Figma size sets.
Overflow
When width is constrained, the label ellipsizes. Icons stay visible. The full label remains the accessible name.
Icons
Pass any React node into iconStart / iconEnd. Icon size follows the button size; placement follows reading direction in RTL. Until EDS ships icons, demos use Quix icons.
States
Loading preserves button width (content is visually hidden) and centers a spinner, matching Figma.
RTL
Wrap the control (or a parent) in dir="rtl" / lang="fa". Flex order places leading content at the inline-start edge.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| tone | "default" | "negative" | "static-black" | "static-white" | "default" | Color intent |
| variant | "primary" | "secondary" | "outline" | "ghost" | "primary" | Visual style |
| size | "sm" | "md" | "lg" | "md" | Height 28px / 36px / 40px |
| type | "button" | "submit" | "reset" | "button" | Native <button type> |
| iconStart | ReactNode | — | Inline-start icon (flips in RTL) |
| iconEnd | ReactNode | — | Inline-end icon (flips in RTL) |
| loading | boolean | false | Spinner + aria-busy |
| disabled | boolean | — | Disabled state |
| className | string | — | Extra classes |
| onClick | (event: React.MouseEvent<HTMLElement, MouseEvent>) => void | — | Click handler |
Accessibility
WCAG success criteria
| Criterion | Level | How Button supports it | |
|---|---|---|---|
| 1.3.1 Info and Relationships | A | Native button conveys role and programmatic relationships; optional aria-* props extend structure for toggles, menus, and described-by links. | |
| 1.4.3 Contrast (Minimum) | AA | Label text uses tokenized foreground/background pairs per tone × variant. Verify on your page background—consumer responsibility for custom surfaces. | |
| 1.4.11 Non-text Contrast | AA | Borders, focus ring, and icon strokes use semantic color tokens with sufficient contrast against adjacent colors in the default theme. | |
| 2.1.1 Keyboard | A | Fully operable with keyboard; Enter and Space activate when enabled. | |
| 2.1.2 No Keyboard Trap | A | Single focus stop; Tab moves in and out without trapping focus. | |
| 2.4.3 Focus Order | A | Follows DOM order; no focus management inside the component. | |
| 2.4.7 Focus Visible | AA | Distinct :focus-visible ring (2px offset + 2px stroke) on keyboard focus. | |
| 2.5.8 Target Size (Minimum) | AA | Default heights (28–40px) exceed the 24×24px minimum. Icon-only or tightly grouped layouts may need extra hit area—your layout responsibility. | |
| 2.5.5 Target Size (Enhanced) | AAA | Not met at default sizes (below 44×44px). Increase spacing, use size="lg", or wrap with padding for motor-accessibility goals. | |
| 4.1.2 Name, Role, Value | A | Native role; name from text or aria-label; disabled and aria-busy reflect state. |
Why it matters
Buttons are the primary affordance for irreversible actions, form submission, and navigation shortcuts. When they are mislabeled, unreachable by keyboard, or visually ambiguous, people who rely on assistive tech or motor precision lose confidence—and products ship avoidable friction. EDS Button renders a native <button> so activation, disabled behavior, and form integration match platform expectations out of the box.
Keyboard
| Key | When focused | Notes |
|---|---|---|
| ⇥ Tab | — | Moves focus to the next focusable control in document order. |
| ⇧ + ⇥ Tab | — | Moves focus to the previous focusable control. |
| ↵ Enter | Enabled button | Activates the button (same as click). |
| Space | Enabled button | Activates the button (native button behavior). |
| ⇥ Tab / ↵ Enter / Space | disabled or loading | No activation — both states set native disabled, remove the control from the tab order, and block pointer input (pointer-events: none). |
The component does not trap focus; it participates in the page’s natural focus order.
Screen readers & semantics
| Element | Semantics |
|---|---|
| Root | Native <button> via Base UI Button — implicit role="button". |
| Accessible name | Visible label text (children), or aria-label when there is no visible text (required for icon-only buttons). |
| Loading | aria-busy="true" while loading is set; native disabled prevents duplicate activation. |
| Spinner | LoadingSpinner is aria-hidden — decorative; progress is conveyed through aria-busy and the preserved label text. |
| Label during load | Label stays in the DOM (opacity 0) so layout width is stable; screen readers still read the button name. |
| Truncation | A constrained width ellipsizes the visible label; children stay in the DOM so the accessible name is still the full string. |
| Icon slots | Wrapper spans set aria-hidden="true". Mark decorative SVGs inside slots with aria-hidden="true" as well. |
| Form & ARIA | Supports type (button / submit / reset), form, name, value, and aria-label, aria-describedby, aria-controls, aria-expanded, aria-haspopup. |
Icon-only buttons must include aria-label (or visible text elsewhere that names the control). Do not rely on icon shape alone.
Focus & visuals
- Focus ring:
:focus-visibledraws a 2px gap + 2px stroke ring outside the control border, using--eds-color-outline-primary(tone-specific overrides for negative and static palettes). The control setsoverflow: visibleso the ring is never clipped. - Disabled / loading: Reduced interactivity with
cursor: not-allowed(disabled) orcursor: wait(loading); visual treatment uses disabled tokens or opacity per tone. - Contrast: Text and fill colors come from EDS semantic tokens tuned for each tone × variant pair. Validate final contrast on your surface—especially static tones on photographic or brand backgrounds.
- RTL:
iconStart/iconEndfollow inline direction; label and icons reorder with flex so reading order matchesdir="rtl". - Target size: Heights are 28px (sm), 36px (md), and 40px (lg) — above the 24×24px WCAG minimum. They do not meet the 44×44px enhanced target without additional padding or layout spacing around the control.
Build with it
Do
- Use visible, action-oriented labels (“Save changes”, not “Click here”).
- Set
aria-labelon icon-only buttons; pair witharia-describedbywhen extra context is needed. - Mark decorative icons
aria-hidden="true"insideiconStart/iconEnd. - Use
loading(not a second click) while async work runs; the label should still describe the action (“Submitting…”). - Use
type="submit"inside forms andtype="button"for in-page actions that must not submit. - Pass
aria-expanded/aria-controlswhen the button opens a menu, dialog, or disclosure you own.
Don’t
- Use a
Buttonwithout an accessible name (empty children and noaria-label). - Rely on color alone to signal destructive actions—pair
tone="negative"with clear text. - Re-enable interaction while
loading; wait until the operation completes. - Swap a disabled button for a
divwith click handlers—usedisabledso assistive tech gets the correct state. - Place icon-only controls closer than ~8px without verifying touch targets on mobile.
// Icon-only — always label
<Button aria-label="Close dialog" iconStart={<IconX />} />
// Loading submit — name stays meaningful
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? "Submitting…" : "Submit"}
</Button>
// Toggle — expose state
<Button
aria-expanded={open}
aria-controls="filters-panel"
onClick={() => setOpen((v) => !v)}
>
Filters
</Button>Quick test
Run this five-minute pass before shipping a flow that uses Button:
- Keyboard only — Tab to each button; confirm a visible focus ring. Press Enter and Space; confirm activation. Tab past
disabledandloadingbuttons—they should be skipped. - Screen reader — With VoiceOver or NVDA, verify each button announces a clear name and “button”. Toggle
loadingand confirm “busy” (or equivalent) is announced without duplicate activation. - Icon-only — Remove visible text; confirm the control is unnamed without
aria-label, then addaria-labeland confirm the name is read. - Zoom 200% — Labels remain visible; focus ring is not clipped.
- RTL — Set
dir="rtl"on a toolbar; confirm icons sit on the inline-start/end edges and labels read in Persian/Arabic order. - Contrast spot-check — Primary, outline, and negative variants on your real page background; adjust surface or tone if text or borders fail 4.5:1 (text) / 3:1 (UI components).