Button
Triggers an action or event. Supports five semantic variants, three sizes, a loading spinner state, and full keyboard accessibility.
AccessibleDark Mode5 Variants3 SizesLoading State
01
Theme Preview
All five variants and three sizes across both themes — forced styling for accurate side-by-side comparison.
Light
Dark
02
Live Demo
Variant
Size
03
Code Snippet
src/ui/Button.tsx
import { Button } from "@/ui/Button"; // Variants <Button.Root variant="primary">Save changes</Button.Root> <Button.Root variant="secondary">Cancel</Button.Root> <Button.Root variant="ghost">Learn more</Button.Root> <Button.Root variant="destructive">Delete account</Button.Root> <Button.Root variant="outline">View details</Button.Root> // Sizes <Button.Root size="sm">Small</Button.Root> <Button.Root size="md">Medium</Button.Root> <Button.Root size="lg">Large</Button.Root> // States <Button.Root isLoading>Saving...</Button.Root> <Button.Root disabled>Unavailable</Button.Root>
Backward compatible: API lama <Button /> tetap didukung, tapi style utama sekarang <Button.Root />.
04
Copy-Paste (Single File)
Button.tsx
import type { ButtonHTMLAttributes, ReactNode } from "react"; type ButtonVariant = "primary" | "secondary" | "ghost" | "destructive" | "outline"; type ButtonSize = "sm" | "md" | "lg"; interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { variant?: ButtonVariant; size?: ButtonSize; isLoading?: boolean; children: ReactNode; } const cn = (...classes: Array<string | undefined | false>) => classes.filter(Boolean).join(" "); const VARIANT_CLASSES: Record<ButtonVariant, string> = { primary: "bg-primary text-white hover:bg-primary/90", secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700", ghost: "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800", destructive: "bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600", outline: "border border-slate-300 text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-800/50", }; const SIZE_CLASSES: Record<ButtonSize, string> = { sm: "text-xs px-3 py-1.5 h-7 gap-1.5", md: "text-sm px-4 py-2 h-9 gap-2", lg: "text-base px-6 py-2.5 h-11 gap-2", }; function Spinner() { return <span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />; } function ButtonRoot({ variant = "primary", size = "md", isLoading = false, disabled, children, className, ...props }: ButtonProps) { return ( <button {...props} disabled={disabled || isLoading} className={cn( "inline-flex items-center justify-center rounded-lg font-semibold transition-all", "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-primary/40", "disabled:cursor-not-allowed disabled:opacity-50", VARIANT_CLASSES[variant], SIZE_CLASSES[size], className, )} > {isLoading && <Spinner />} {children} </button> ); } type ButtonCompound = typeof ButtonRoot & { Root: typeof ButtonRoot; Spinner: typeof Spinner }; export const Button = Object.assign(ButtonRoot, { Root: ButtonRoot, Spinner }) as ButtonCompound;
05
Props
Extends all native HTMLButtonElement attributes (onClick, type, form, etc.).
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "primary" | "secondary" | "ghost" | "destructive" | "outline" | "primary" | Visual style of the button. |
size | "sm" | "md" | "lg" | "md" | Controls height, padding, and font size. |
isLoading | boolean | false | Shows a spinner and disables the button while true. |
disabled | boolean | false | Disables interaction and reduces opacity. |
children | ReactNode | — | Button label content. |
className | string | — | Extra CSS classes merged via cn(). |