Redpanda UIRedpanda UI
Components

Button

Displays a button or a component that looks like a button.

Made by shadcn

Powered by

Installation

Loading component…

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.

Loading component…

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:

  1. A variant is named after the token it paints. primary paints --color-primary, warning paints the warning tone. No name means a different thing here than it does on a Badge.
  2. The neutral member is named after its family, not its tone. default, outline, ghost and dashed are each the neutral member of their family; all four paint secondary-*. Naming them after the tone would mean an unstyled control has to name one.
  3. The default is the neutral one. A plain <Button> is default, so primary is opt-in — an unstyled control should not claim the page's one loud slot.
  4. 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, so success and informative are 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:

FamilyNeutralActionBrandWarningDestructiveBorrows ink
Filleddefaultprimarybrandwarningdestructiveinverse*
Outlineoutlineprimary-outlinebrand-outlinewarning-outlinedestructive-outlinecurrent-outline
Ghostghostprimary-ghostbrand-ghostwarning-ghostdestructive-ghostcurrent-ghost
Dasheddashedprimary-dashedbrand-dashedwarning-dasheddestructive-dashedcurrent-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.

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

  1. Filled — primary actions, highest emphasis
  2. Outline — secondary actions, medium emphasis
  3. 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/Updateoutline
  • Deletedestructive-outline
  • Confirm delete — filled destructive, in the confirmation dialog
Loading component…

Form and Multi-Step Flow Patterns

  • Submit/Next/Continue — filled primary, one per view
  • Secondary actions (save draft, skip) — outline
  • Back/Cancelghost

Escape routes stay quiet; the fill always means forward.

Loading component…

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. Use destructive-outline for 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 from surface-destructive on 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.

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 Button primitive, rendering a <button> unless render says otherwise
  • Stylingclass-variance-authority, one variant × size pair 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-busy handled for you

Examples

Variants

Loading component…

Sizes

Loading component…

Dashed

Loading component…
Loading component…

Icon

Loading component…

With Icon

Loading component…

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.

Loading component…
Loading component…
Loading component…

As Child

Loading component…

Router Integration

Loading component…

With keyboard shortcut

Loading component…

With status dot

Loading component…

Credits

  • We take our inspiration from Shadcn UI for the button component and style.

Recent changes

  • patchv1.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
  • minorv1.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
  • minorv1.1.0Theme docs refresh, readability pass on semantic foregrounds, and consumer-facing Base UI regression fixes.#121
  • minorv1.0.0Post-Base-UI polish. Public API unchanged.#116
  • majorv1.0.0Migrate every Radix-based primitive to `@base-ui/react@^1.4.0` (Base UI).#114
See full history →
Built by malinskibeniamin. The source code is available on GitHub.

On this page