Skip to content

Inputs

Button

Buttons trigger an action or, given an href, navigate like a link. Six variants cover everything from the one primary action on a screen to quiet toolbar icons, in three heights that line up with every other control (32, 36 and 40px). Loading, icons and full width are built in, so you never fight the base classes.

On this page

Usage

components/ui/button.tsx
import { Button } from "@/components/ui/button"; export function Example() {  return <Button variant="primary">Create invoice</Button>;}

Examples

Variants

Use one primary button per view. Secondary is the default; soft and ghost suit dense UIs, danger is for destructive actions only.

Sizes

sm, md and lg match the input heights. The icon sizes are square and need an aria-label.

With icons

Pass icons through iconLeft and iconRight; padding on that side tightens automatically.

Loading

loading swaps the leading icon for a spinner, disables the button and sets aria-busy. The label stays, so the width doesn't jump.

Button group

ButtonGroup joins related actions into one control. Give it an aria-label.

In context

A destructive confirmation footer: full-width on phones, natural width from sm up, disabled state included.

Delete “Fernhill Studio”?

The workspace, 12 projects and 48 invoices will be removed for everyone. This can't be undone.

Props

Button

  • variant"primary" | "secondary" | "outline" | "ghost" | "soft" | "danger"default: "secondary"Visual weight. Keep one primary per view.
  • size"sm" | "md" | "lg" | "icon-sm" | "icon" | "icon-lg"default: "md"Height 32 / 36 / 40px. Icon sizes are square.
  • loadingbooleandefault: falseShows a spinner, disables the button and sets aria-busy.
  • iconLeftReactNodedefault: —Icon before the label. Replaced by the spinner while loading.
  • iconRightReactNodedefault: —Icon after the label, e.g. an arrow or chevron.
  • fullWidthbooleandefault: falseStretch to the container width.
  • hrefstringdefault: —Render a Next.js Link with button styling instead of a <button>.
  • type"button" | "submit" | "reset"default: "button"Native button type. Defaults to button so forms don't submit by accident.
  • ...propsButtonHTMLAttributesdefault: —Any other <button> attribute (onClick, disabled, aria-*).

ButtonGroup

  • orientation"horizontal" | "vertical"default: "horizontal"Direction the buttons are joined in.
  • aria-labelrequiredstringdefault: —Describes the group for screen readers.
  • childrenrequiredReactNodedefault: —Buttons (or anything with matching borders).

Accessibility

  • Renders a native <button type="button">, so Enter and Space activate it and it never submits a form unless you pass type="submit".
  • Icon-only buttons have no visible text: always pass an aria-label ("Settings", "Remove file").
  • loading sets aria-busy and disables the button so a second click can't send twice; keep the label meaningful while it spins.
  • Focus shows a 3px ring in the accent color on keyboard focus only (focus-visible), the same ring every Plinth control uses.
  • ButtonGroup renders role="group"; give it an aria-label so the shared purpose is announced.
  • Danger is never the only cue: pair it with explicit copy such as "Delete workspace".

Suggested