Button
Displays a button or a component that looks like a button.
Made by shadcnInstallation
Usage
import { Button } from "@/components/redpanda-ui/button"
<Button>Click me</Button>
<Button variant="outline">Outline</Button>
<Button variant="destructive">Delete</Button>
<Button size="sm">Small</Button>
<Button size="icon"><Plus /></Button>
// Keeps its width while a centred spinner overlays the content
<Button isLoading>Save changes</Button>
// Render as any element, including a router link
<Button render={<Link href="/dashboard" />}>Go to Dashboard</Button>Variants and States
Every variant across every state. The hover, focus and active columns are static copies of the real
state classes, so they can be compared without interacting; inverse and current-* sit on a filled
row, which is where they are meant to be used.
Tones
Four families — filled, outline, ghost, dashed — each in the same five tones. 25 variants, no two of them the same style. Four rules keep it that way:
- A variant is named after the token it paints.
primarypaints--color-primary,warningpaints the warning tone. No name means a different thing here than it does on a Badge. - The neutral member is named after its family, not its tone.
default,outline,ghostanddashedare each the neutral member of their family; all four paintsecondary-*. Naming them after the tone would mean an unstyled control has to name one. - The default is the neutral one. A plain
<Button>isdefault, soprimaryis opt-in — an unstyled control should not claim the page's one loud slot. - Button carries action tones, Badge carries status tones. A tone belongs here only if you can
phrase an action in it —
brand(a brand CTA),warning("proceed anyway"),destructive. You cannot ask someone to succeed, sosuccessandinformativeare Badge-and-Alert tones only.
Every family reads in the same order — neutral first, then the tones, then the one that borrows its ink from the ground:
| Family | Neutral | Action | Brand | Warning | Destructive | Borrows ink |
|---|---|---|---|---|---|---|
| Filled | default | primary | brand | warning | destructive | inverse* |
| Outline | outline | primary-outline | brand-outline | warning-outline | destructive-outline | current-outline |
| Ghost | ghost | primary-ghost | brand-ghost | warning-ghost | destructive-ghost | current-ghost |
| Dashed | dashed | primary-dashed | brand-dashed | warning-dashed | destructive-dashed | current-dashed |
* inverse flips its own fill and ink with the theme rather than borrowing the ground's — it holds
the filled family's last slot but is not a current-*.
Within a row the tones are interchangeable; across rows the emphasis drops. link sits outside the
grid: it is the action colour by definition and has no neutral counterpart.
primary and secondary changed meaning, and seven spellings were removed. These names used to
be emphasis ranks that painted the opposite token: variant="primary" filled from
--color-secondary, and secondary-outline / -ghost / -dashed drew in primary-*. They now
paint what they are named after, matching Badge, the tokens and the shadcn baseline.
A plain <Button> is unaffected — the default moved from primary to default, which paints
the same neutral fill an unstyled button always had.
| Was | Now |
|---|---|
variant="primary" (neutral fill) | variant="default", or drop the prop |
variant="secondary" (action fill) | variant="primary" |
secondary-outline · -ghost · -dashed | primary-outline · -ghost · -dashed |
accent · accent-outline · -ghost · -dashed | brand · brand-outline · -ghost · -dashed |
informative-* · success-* | use a Badge or Alert; they are not action tones |
Composing Buttons
The One Filled Button Rule
One filled button per screen or context. It is the action you want taken — submit, create, continue. More than one dilutes all of them.
Button Hierarchy
- Filled — primary actions, highest emphasis
- Outline — secondary actions, medium emphasis
- Ghost — tertiary actions, lowest emphasis
outline is the workhorse: its visible border reads as more clickable than a ghost while staying
clearly secondary to a fill.
CRUD Action Patterns
- Create — filled
primary - Edit/Update —
outline - Delete —
destructive-outline - Confirm delete — filled
destructive, in the confirmation dialog
Form and Multi-Step Flow Patterns
- Submit/Next/Continue — filled
primary, one per view - Secondary actions (save draft, skip) —
outline - Back/Cancel —
ghost
Escape routes stay quiet; the fill always means forward.
Content Guidelines
Text or an icon, never both. Icon-only buttons are for universally understood actions (close, search, menu) and always need an accessible label.
When to use
Variant Guidelines
default— the neutral filled button, and what a bare<Button>renders: the fallback when an action is neither the one forward step nor a supporting one.primary— forward-moving actions: submit, create, continue, save. One per context.outline— the neutral secondary button, and the one to reach for most: edit actions, secondary form actions, anything supporting that still needs to read as clickable.ghost— tertiary actions that shouldn't dominate: back/cancel, icon buttons, and controls in data-dense surfaces like tables and navigation.dashed— "add new" and placeholder actions. Unshadowed, so it reads as an outline waiting to be filled rather than a raised control.destructive— permanent deletion, in confirmation dialogs only. Usedestructive-outlinefor inline deletes in tables, cards and lists.primary-*— the interactive-coloured member of any family. Reach for it when a secondary or tertiary action should read as coloured rather than neutral.brand-*— the logo red, for brand moments: marketing bands, onboarding, empty-state CTAs. Not a general-purpose emphasis: it sits ΔE 4.9 fromsurface-destructiveon a light page, so keep it out of rows that also hold a destructive action.warning-*— a caution action: "proceed anyway", "override", acknowledging a risky state. Same tone as a warning Badge or Alert, so it reads as part of that message rather than a second accent.current-*— takes the ink of whatever it sits on, for a surface the button cannot know about: a banner, a coloured panel.link— actions that behave like navigation: going somewhere rather than doing something.
Link
Use the buttonVariants helper to style a link as a button:
import { buttonVariants } from "@/components/redpanda-ui/button"
<Link className={buttonVariants({ variant: "outline" })}>Click here</Link>Or hand any router's link to render, which works with TanStack, React Router, Next.js and Remix:
<Button render={<Link href="/login" />}>Login</Button>Anatomy
- Base — Base UI's
Buttonprimitive, rendering a<button>unlessrendersays otherwise - Styling —
class-variance-authority, onevariant×sizepair per call - Content — text or an icon, gapped and auto-sized against the chosen
size(see Content Guidelines) - Accessibility — focus-visible ring, disabled and
aria-busyhandled for you
Examples
Variants
Sizes
Dashed
Link
Icon
With Icon
Loading
isLoading keeps the button's width — the children stay in the layout, visually hidden, while a
centred spinner overlays them. Interaction is blocked and aria-busy is set.
As Child
Router Integration
With keyboard shortcut
With status dot
Credits
- We take our inspiration from Shadcn UI for the button component and style.
Recent changes
- v1.2.0Pin shipped dependency floors to the version we develop against. Registry items now declare ranges like `^5.1.9` (the actual installed version) instead of collapsing to `^5.0.0`, so consumers start on the known-tested baseline while caret semantics still allow any compatible release within the same major.#133
- v1.2.0Add `isLoading` prop to `Button`. Renders a centered spinner overlay while preserving the button's natural width, blocks interaction, and sets `aria-busy`. New playground/docs demos showcase the state across variants and sizes.#129
- v1.1.0Theme docs refresh, readability pass on semantic foregrounds, and consumer-facing Base UI regression fixes.#121
- v1.0.0Post-Base-UI polish. Public API unchanged.#116
- v1.0.0Migrate every Radix-based primitive to `@base-ui/react@^1.4.0` (Base UI).#114