Gesso
Actions

Button

An action button with six variants, three sizes, icon slots, and a loading state.

Button runs an action: it saves, submits, opens, or deletes. It takes icons as components and sizes them to match, holds itself while an action is in flight, and renders as a link or any other element with asChild. Its corners follow the look's rounded-control, so it is an 8px rectangle in Washi and a pill in ICS.

Usage

import { Plus } from '@phosphor-icons/react';
import { Button } from '@/components/ui/button';

<Button iconBefore={Plus} onClick={createIssue}>
  New issue
</Button>

Pick the variant by weight

Use one default button for the main action in a view, and secondary for the actions beside it. outline and ghost are quieter, for toolbars, dense rows, and actions that repeat down a list. destructive is for an action that deletes or cannot be undone, usually inside a Confirm. link reads as text, for a minor action inside a sentence.

Tone is the hue

A variant is a weight, and a tone is a hue, so any weight can wear any of a button's hues. variant="destructive" is a shorthand for the default weight in the danger tone, and <Button variant="secondary" tone="danger"> is a quiet red button. A button takes neutral, primary, support (the second brand hue), and danger. Success, warning, and info are feedback hues for an Alert, Badge, or Progress, so a button does not take them. Each variant has its own default: default and link are primary, destructive is danger, and secondary, outline, and ghost are neutral.

Every Button sets its own tone, so a button inside a danger Alert stays neutral unless you ask. Pass tone="inherit" to take the tone of the surrounding scope, which is how an alert's action draws in the alert's hue. Color explains the tokens.

Three sizes

sm is 36px tall, md is 40px and the default, and xl is 56px, for a hero or an empty state. On a touch screen every size grows to at least 44px tall, so a small button is still easy to hit.

Icons are components

iconBefore and iconAfter take an icon component rather than an element, so the button can size it: 16px at sm, and 20px at md and xl. Pass iconSize to set your own. A leading icon with no trailing one gets a little more padding on the right, so the label does not sit off center. showIconBefore and showIconAfter hide an icon without removing the prop, for a button that changes with the layout.

Because the prop is a component, set it from a client component. A Server Component cannot pass a component to a client one, so <Button iconBefore={Plus}> in a page file fails. Put that button in a small "use client" file instead.

Loading holds the button

loading swaps the icons for a spinner and disables the button until you set it back, so a second click cannot send the action twice. Change the label too, from "Save" to "Saving", because the spinner is the only other sign. Click Save to watch it.

asChild hands the button's styling to its one child and renders that instead, so a link that looks like a button is still a link.

import Link from 'next/link';

<Button asChild variant="outline">
  <Link href="/docs">Read the docs</Link>
</Button>

The child renders as it is. The icon slots and the spinner are skipped, because the slot takes exactly one child, so put an icon inside the link yourself. A link has no disabled state either, so disabled and loading do not stop it.

Props

Button takes every <button> attribute, plus these.

PropTypeDefaultWhat it does
variant"default" | "secondary" | "outline" | "ghost" | "destructive" | "link""default"The weight of the action. default is the primary fill.
tone"neutral" | "primary" | "support" | "danger" | "inherit"By variantThe hue. inherit takes the tone of the surrounding scope.
size"sm" | "md" | "xl""md"36, 40, or 56px tall, and at least 44px on a touch screen.
iconBefore / iconAfterElementTypeAn icon component drawn before or after the label, sized to the button.
showIconBefore / showIconAfterbooleanOn when the icon is setShows or hides the icon without removing the prop.
iconSizenumber | stringFollows sizeSets the icon's width and height.
loadingbooleanfalseShows a spinner in place of the icons and disables the button.
asChildbooleanfalseRenders its one child with the button's styling, instead of a <button>.
type"button" | "submit" | "reset""button"As on <button>, except it never submits a form unless you set "submit".

To style something that is not a Button, buttonVariants({ variant, size }) returns the classes. Its sizes are the root's, sm, lg, and xl, where lg is Button's md.

Accessibility

  • It renders a native <button>, so Enter and Space press it, and type="button" keeps it from submitting a form by accident.
  • Focus shows a 2px ring on :focus-visible only, offset from the edge, in the contrast ink rather than the brand color, so it reads on every variant and look.
  • In forced colors the button keeps a transparent outline that the system paints, so focus stays visible in Windows high contrast.
  • On a touch screen it is at least 44px tall at every size.
  • disabled and loading set the native disabled attribute, so the button leaves the tab order and assistive tech reports it as unavailable. Nothing is announced when loading starts, so let the label say what is happening.
  • The accessible name is the label. An icon adds nothing to it, so a button with only an icon needs Icon button, whose label becomes the name and a tooltip.
  • With asChild the child keeps its own role, so a link is announced as a link.
  • Under reduced motion the hover color changes without a transition. The loading spinner keeps turning, since it is the visible sign that the action is still running.

When to use something else

  • Icon button for an icon with no text.
  • Toggle for something that stays on or off, like bold or a filter.
  • Button group for buttons joined into one control.
  • A plain link for navigation that should read as text. Reach for asChild only when it should look like a button.

Install

shadcn add @gesso/button

On this page