Button
The canonical button. Mirrors shadcn Button. The single most-used component — its island story matters.
Purpose
The canonical button. Mirrors shadcn Button. The single most-used component — its island story matters.
Island model
island: "subtree". Two delivery modes:
- Server button — when used as a link-like or form-submit (
type="submit"), noonClick. Server-rendered, zero JS. - Island button — when wrapped in an island parent (Dialog, Form, etc.) OR when the page mounts an island that depends on a click handler. The component itself does not auto-island; instead it inherits its parent island root.
Pages that use Buttons only as link/submit ship zero JS for them.
Peer dependencies
class-variance-authority@^0.7
Public API
export type ButtonVariant = "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
export type ButtonSize = "default" | "sm" | "lg" | "icon"
export function Button(props: {
variant?: ButtonVariant
size?: ButtonSize
asChild?: boolean
className?: string
} & React.ButtonHTMLAttributes<HTMLButtonElement>): JSX.Element
export const buttonVariants: (opts: { variant?: ButtonVariant; size?: ButtonSize }) => string
export const island = "subtree"
Patties adjustments
buttonVariantsis exported so other components (e.g.AlertDialogAction) can compose classes without depending onButton.asChilduses_internal/slot.tsx.- Drops
forwardRef.
Phase-1 inline-styling debt to resolve here
Phase 1 shipped before this spec existed, so two phase-1 components inlined their own minimal button cva rather than depend on a not-yet-built Button:
- Pagination —
PaginationLinkinlines a localcvaand a localPaginationLinkSizeunion instead of importingbuttonVariants/ButtonSize. - Input Group —
InputGroupButtoninlinesinputGroupButtonVariantsinstead of composingbuttonVariants.
Both templates carry a // Phase 1 inlines button styling… comment marking the spot. When implementing Button, migrate these to import buttonVariants (and ButtonSize) so there is a single source of truth for button styling. This is the reason buttonVariants is exported as a standalone.
Acceptance criteria
- All variant × size combinations snapshot correctly.
asChildwith an<a>child renders an anchor with the button classes and forwards focus styles.- A button with
type="submit"inside a<form action="/x" method="post">works without any client JS. - A button with
onClickinside an island parent hydrates and the handler fires. - Pagination's
PaginationLinkand Input Group'sInputGroupButtonare refactored to composebuttonVariantsinstead of their inlined localcva, with their phase-1// Phase 1 inlines button stylingcomments removed.