EDS 0.x
Components

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

PropTypeDefaultDescription
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>
iconStartReactNodeInline-start icon (flips in RTL)
iconEndReactNodeInline-end icon (flips in RTL)
loadingbooleanfalseSpinner + aria-busy
disabledbooleanDisabled state
classNamestringExtra classes
onClick(event: React.MouseEvent<HTMLElement, MouseEvent>) => voidClick handler

Accessibility

WCAG success criteria

CriterionLevelHow Button supports it
1.3.1 Info and RelationshipsANative button conveys role and programmatic relationships; optional aria-* props extend structure for toggles, menus, and described-by links.
1.4.3 Contrast (Minimum)AALabel text uses tokenized foreground/background pairs per tone × variant. Verify on your page background—consumer responsibility for custom surfaces.
1.4.11 Non-text ContrastAABorders, focus ring, and icon strokes use semantic color tokens with sufficient contrast against adjacent colors in the default theme.
2.1.1 KeyboardAFully operable with keyboard; Enter and Space activate when enabled.
2.1.2 No Keyboard TrapASingle focus stop; Tab moves in and out without trapping focus.
2.4.3 Focus OrderAFollows DOM order; no focus management inside the component.
2.4.7 Focus VisibleAADistinct :focus-visible ring (2px offset + 2px stroke) on keyboard focus.
2.5.8 Target Size (Minimum)AADefault 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)AAANot 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, ValueANative 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

KeyWhen focusedNotes
⇥ TabMoves focus to the next focusable control in document order.
+ ⇥ TabMoves focus to the previous focusable control.
↵ EnterEnabled buttonActivates the button (same as click).
SpaceEnabled buttonActivates the button (native button behavior).
⇥ Tab / ↵ Enter / Spacedisabled or loadingNo 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

ElementSemantics
RootNative <button> via Base UI Button — implicit role="button".
Accessible nameVisible label text (children), or aria-label when there is no visible text (required for icon-only buttons).
Loadingaria-busy="true" while loading is set; native disabled prevents duplicate activation.
SpinnerLoadingSpinner is aria-hidden — decorative; progress is conveyed through aria-busy and the preserved label text.
Label during loadLabel stays in the DOM (opacity 0) so layout width is stable; screen readers still read the button name.
TruncationA constrained width ellipsizes the visible label; children stay in the DOM so the accessible name is still the full string.
Icon slotsWrapper spans set aria-hidden="true". Mark decorative SVGs inside slots with aria-hidden="true" as well.
Form & ARIASupports 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-visible draws 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 sets overflow: visible so the ring is never clipped.
  • Disabled / loading: Reduced interactivity with cursor: not-allowed (disabled) or cursor: 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 / iconEnd follow inline direction; label and icons reorder with flex so reading order matches dir="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-label on icon-only buttons; pair with aria-describedby when extra context is needed.
  • Mark decorative icons aria-hidden="true" inside iconStart / iconEnd.
  • Use loading (not a second click) while async work runs; the label should still describe the action (“Submitting…”).
  • Use type="submit" inside forms and type="button" for in-page actions that must not submit.
  • Pass aria-expanded / aria-controls when the button opens a menu, dialog, or disclosure you own.

Don’t

  • Use a Button without an accessible name (empty children and no aria-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 div with click handlers—use disabled so 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:

  1. Keyboard only — Tab to each button; confirm a visible focus ring. Press Enter and Space; confirm activation. Tab past disabled and loading buttons—they should be skipped.
  2. Screen reader — With VoiceOver or NVDA, verify each button announces a clear name and “button”. Toggle loading and confirm “busy” (or equivalent) is announced without duplicate activation.
  3. Icon-only — Remove visible text; confirm the control is unnamed without aria-label, then add aria-label and confirm the name is read.
  4. Zoom 200% — Labels remain visible; focus ring is not clipped.
  5. RTL — Set dir="rtl" on a toolbar; confirm icons sit on the inline-start/end edges and labels read in Persian/Arabic order.
  6. 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).

On this page