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
Install
$
npx react-principles add button01
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 variant="primary">Save changes</Button> <Button variant="secondary">Cancel</Button> <Button variant="ghost">Learn more</Button> <Button variant="destructive">Delete account</Button> <Button variant="outline">View details</Button> // Sizes <Button size="sm">Small</Button> <Button size="md">Medium</Button> <Button size="lg">Large</Button> // States <Button isLoading>Saving...</Button> <Button disabled>Unavailable</Button>
Backward compatible: API lama <Button /> tetap didukung, tapi style utama sekarang <Button />.
04
Copy-Paste (Single File)
Button.tsx
import type { ButtonHTMLAttributes, ReactNode } from "react"; import { cn } from "@/lib/utils"; export type ButtonVariant = "primary" | "secondary" | "ghost" | "destructive" | "outline"; export type ButtonSize = "sm" | "md" | "lg"; export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { variant?: ButtonVariant; size?: ButtonSize; isLoading?: boolean; children: ReactNode; } const VARIANT_CLASSES: Record<ButtonVariant, string> = { primary: "bg-primary text-white hover:bg-primary/90 focus-visible:ring-primary/40", secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 focus-visible:ring-slate-400/40", ghost: "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800 focus-visible:ring-slate-400/40", destructive: "bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600 focus-visible:ring-red-500/40", 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 focus-visible:ring-slate-400/40", }; 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 ( <svg className="w-4 h-4 animate-spin shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /> </svg> ); } export function Button({ 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 font-semibold rounded-lg transition-all", "focus-visible:outline-hidden focus-visible:ring-2", "disabled:opacity-50 disabled:cursor-not-allowed", VARIANT_CLASSES[variant], SIZE_CLASSES[size], className, )} > {isLoading && <Spinner />} {children} </button> ); }
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(). |