Skip to content

Button component

Button is the pressable primitive nearly everything else in the library builds on — plain actions, form submits, links styled as buttons, and icon-only controls all go through the same component. It renders a native <button> by default and switches to an <a> automatically the moment you pass href, so you never have to choose the element yourself.

Actions26 propsAccessibility notes

Playground

Change a prop on the right and the preview updates. Switch to Code to copy what you see.

Recipes

Copy-paste patterns for the ways Button actually gets used — each one a real, self-contained example.

Variants

Five visual weights, from a filled solid down to an inline link — reach for lighter variants as the action gets more secondary.

<Button>Solid</Button>
<Button variant="soft">Soft</Button>
<Button variant="bordered">Bordered</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>

Semantic colors

Colour communicates intent — primary for the main action, success/warning/danger for outcomes. All read from your theme tokens.

<Button color="primary">Primary</Button>
<Button color="success">Success</Button>
<Button color="warning">Warning</Button>
<Button color="danger">Delete</Button>

Sizes

Six size tokens from xs to xl. Pick by density — xs/sm for toolbars and tables, lg/xl for landing-page calls to action.

<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra large</Button>

With icons

Add a leading or trailing icon with startContent / endContent — spacing and sizing are handled for you. (Bring your own SVG components.)

<Button startContent={<PlusIcon />}>New item</Button>
<Button variant="bordered" endContent={<ArrowRightIcon />}>
  Continue
</Button>

Icon-only

Set isIconOnly for a square control that holds just an icon — always pass an aria-label so it stays accessible.

<Button isIconOnly aria-label="Add">
  <PlusIcon />
</Button>
<Button isIconOnly variant="bordered" aria-label="Download">
  <DownloadIcon />
</Button>

Loading state

isLoading swaps in a spinner and disables the button so it can't be double-submitted. Move the spinner with spinnerPlacement.

<Button isLoading>Saving…</Button>
<Button isLoading variant="bordered" spinnerPlacement="end">
  Processing
</Button>

Full width

fullWidth stretches the button to its container — the norm for forms and mobile CTAs.

<div className="max-w-sm">
  <Button fullWidth color="primary">Create account</Button>
</div>

Variant and color

variant sets how much visual weight a button carries: solid (the default) is a filled, high-contrast action; soft is a tinted background for secondary actions that shouldn't compete with a solid primary button nearby; bordered keeps just an outline; ghost has no background or border until hovered; link strips button chrome entirely and reads as an inline text link. color is one of seven semantic roles and works the same way across every variant.

<Button variant="solid" color="primary">Save</Button>
<Button variant="soft" color="default">Cancel</Button>
<Button variant="bordered" color="danger">Delete</Button>
<Button variant="ghost" color="default">Skip</Button>
<Button variant="link" color="primary">Learn more</Button>

Pass href and Button renders a real anchor instead of a button element — no wrapping a Button inside a Link and fighting duplicate focus rings. target/rel/download all forward straight to the anchor, and rel defaults to "noopener noreferrer" automatically whenever target="_blank" is set, so external links can't be quietly forgotten. For a client-side router's own Link component, use the polymorphic as prop instead.

<Button href="/pricing" variant="soft">View pricing</Button>
<Button href="https://github.com/oks-ui" target="_blank">GitHub ↗</Button>

// Polymorphic: render as next/link (or any component) instead of <a>/<button>
<Button as={Link} href="/dashboard" color="primary">
  Go to dashboard
</Button>

Loading and disabled states

isLoading swaps in a spinner (spinnerPlacement controls which side of the label it sits on, or pass your own spinner node) and sets aria-busy — but the button stays focusable and enabled on purpose, so a screen reader user tabbed onto it doesn't lose their place mid-action. Use isDisabled separately when the action genuinely shouldn't be reachable at all; disabling removes it from the tab order and, on an anchor-rendered button, adds aria-disabled since native <a> has no disabled attribute.

<Button isLoading color="primary">Saving…</Button>
<Button isDisabled variant="soft">Unavailable</Button>

Icon-only buttons

For a button that's just an icon, set isIconOnly (which switches to a square footprint sized off the current size scale) and always pass an aria-label — there's no visible text for assistive tech to fall back on otherwise.

<Button isIconOnly variant="ghost" aria-label="Close">
  <XIcon />
</Button>

Press events, not just onClick

Button forwards native onClick as usual, but also exposes onPress/onPressStart/onPressEnd/onPressUp/onPressChange for cases that care about the full press lifecycle (press-and-hold interactions, custom ripple-adjacent effects, distinguishing a press that started on the button but was dragged off before release). Reach for plain onClick unless you specifically need one of those finer-grained moments.

Props

The full public API for Button. Every prop is stable and intentionally typed.

Content

children

ReactNode

default: —

Button label/content.

startContent

ReactNode

default: —

Content rendered before the label.

endContent

ReactNode

default: —

Content rendered after the label.

spinner

ReactNode

default: built-in spinner

Overrides the default loading spinner.

spinnerPlacement

"start" | "end"

default: start

Where the loading spinner renders relative to the label.

Appearance

variant

"solid" | "soft" | "bordered" | "ghost" | "link"

default: solid

Visual style variant.

color

"default" | "primary" | "secondary" | "info" | "success" | "warning" | "danger"

default: default

Semantic color.

colorDepth

50 | 100 | ... | 950

default: 500

Shade depth for the selected color.

size

"xs" | "xs-sm" | "sm" | "md" | "lg" | "xl"

default: md

Button size.

radius

"none" | "xs" | "xs-sm" | "sm" | "md" | "lg" | "full"

default: md

Border radius.

shadow

"none" | "xs" | "xs-sm" | "sm" | "md" | "lg"

default: none

Box-shadow depth.

Layout & positioning

fullWidth

boolean

default: false

Expands to fill available horizontal space.

isIconOnly

boolean

default: false

Renders a square icon button. Requires aria-label.

Behavior & states

as

ElementType

default: href ? "a" : "button"

Render as a different element or component.

href

string

default: —

Renders as an anchor when set.

target

string

default: —

Anchor target.

rel

string

default: "noopener noreferrer" when target="_blank"

Anchor rel attribute.

download

string | boolean

default: —

Anchor download attribute.

tooltip

ReactNode | boolean | (Omit<TooltipProps, "children"|"content"> & { content?: ReactNode })

default: —

Wraps the button in the built-in Tooltip component when set. A plain value is shorthand content; bare true derives content from aria-label; an object accepts any Tooltip prop plus an optional content override. Omitted entirely by default.

isDisabled

boolean

default: false

Disables the button; sets aria-disabled + tabIndex=-1 when rendered as an anchor.

isLoading

boolean

default: false

Shows a spinner and sets aria-busy; stays focusable by design.

Motion

disableRipple

boolean

default: false

Disables the click ripple effect.

disableAnimation

boolean

default: false

Disables all transitions/animations, including the ripple.

Events

onPress / onPressStart / onPressEnd / onPressUp / onPressChange

(e) => void

default: —

Press-style callbacks alongside native onClick.

Styling

className

string

default: —

Class applied to the root element.

style

CSSProperties

default: —

Inline styles for the root element.

Accessibility

  • Renders semantic <button type="button"> or <a> — never a styled <div> — so it's keyboard-operable and correctly announced without any extra ARIA wiring.
  • isLoading sets aria-busy="true" but deliberately leaves the button enabled and focusable, since disabling it mid-action would silently drop focus for a keyboard/screen-reader user.
  • isDisabled sets the native disabled attribute on a <button>, or aria-disabled + tabIndex={-1} when rendered as an anchor (href set), since <a> has no native disabled state.
  • isIconOnly buttons have no visible label — always pass your own aria-label so the control has an accessible name.

Best practices

  • Reserve variant="solid" for the one primary action in a given area — a screen with five solid buttons gives the eye nothing to anchor on.
  • Prefer isLoading over swapping the button out for a spinner entirely; keeping the button in place (just busy) avoids a layout shift and a lost focus target.
  • Use href (or as) instead of wrapping Button in a separate anchor/Link — the polymorphic behavior exists specifically so you never end up with two nested interactive elements.
  • Always pair isIconOnly with aria-label; if the icon also has a visible tooltip, the label should still describe the action on its own since tooltips aren't reliably read by every assistive technology.
  • For a set of related actions that should look like one connected control (Prev/Next, view toggles), use ButtonGroup instead of manually managing spacing and border-radius between adjacent Buttons.