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.
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>As a link
Pass as={Link} (or any element/component) and Button renders a real anchor — full button styling, correct link semantics, keyboard and right-click behaviour.
import Link from "next/link";
<Button as={Link} href="/docs/installation">Get started</Button>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>Rendering as a link
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
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Button label/content. |
| startContent | ReactNode | — | Content rendered before the label. |
| endContent | ReactNode | — | Content rendered after the label. |
| spinner | ReactNode | built-in spinner | Overrides the default loading spinner. |
| spinnerPlacement | "start" | "end" | start | Where the loading spinner renders relative to the label. |
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
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "solid" | "soft" | "bordered" | "ghost" | "link" | solid | Visual style variant. |
| color | "default" | "primary" | "secondary" | "info" | "success" | "warning" | "danger" | default | Semantic color. |
| colorDepth | 50 | 100 | ... | 950 | 500 | Shade depth for the selected color. |
| size | "xs" | "xs-sm" | "sm" | "md" | "lg" | "xl" | md | Button size. |
| radius | "none" | "xs" | "xs-sm" | "sm" | "md" | "lg" | "full" | md | Border radius. |
| shadow | "none" | "xs" | "xs-sm" | "sm" | "md" | "lg" | none | Box-shadow depth. |
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
| Prop | Type | Default | Description |
|---|---|---|---|
| fullWidth | boolean | false | Expands to fill available horizontal space. |
| isIconOnly | boolean | false | Renders a square icon button. Requires aria-label. |
fullWidth
boolean
default: false
Expands to fill available horizontal space.
isIconOnly
boolean
default: false
Renders a square icon button. Requires aria-label.
Behavior & states
| Prop | Type | Default | Description |
|---|---|---|---|
| as | ElementType | href ? "a" : "button" | Render as a different element or component. |
| href | string | — | Renders as an anchor when set. |
| target | string | — | Anchor target. |
| rel | string | "noopener noreferrer" when target="_blank" | Anchor rel attribute. |
| download | string | boolean | — | Anchor download attribute. |
| tooltip | ReactNode | boolean | (Omit<TooltipProps, "children"|"content"> & { content?: ReactNode }) | — | 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 | false | Disables the button; sets aria-disabled + tabIndex=-1 when rendered as an anchor. |
| isLoading | boolean | false | Shows a spinner and sets aria-busy; stays focusable by design. |
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
| Prop | Type | Default | Description |
|---|---|---|---|
| disableRipple | boolean | false | Disables the click ripple effect. |
| disableAnimation | boolean | false | Disables all transitions/animations, including the ripple. |
disableRipple
boolean
default: false
Disables the click ripple effect.
disableAnimation
boolean
default: false
Disables all transitions/animations, including the ripple.
Events
| Prop | Type | Default | Description |
|---|---|---|---|
| onPress / onPressStart / onPressEnd / onPressUp / onPressChange | (e) => void | — | Press-style callbacks alongside native onClick. |
onPress / onPressStart / onPressEnd / onPressUp / onPressChange
(e) => void
default: —
Press-style callbacks alongside native onClick.
Styling
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Class applied to the root element. |
| style | CSSProperties | — | Inline styles for the root element. |
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.