GitHub

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 button
01

Theme Preview

All five variants and three sizes across both themes — forced styling for accurate side-by-side comparison.

Light
Dark
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.).

PropTypeDefaultDescription
variant"primary" | "secondary" | "ghost" | "destructive" | "outline""primary"Visual style of the button.
size"sm" | "md" | "lg""md"Controls height, padding, and font size.
isLoadingbooleanfalseShows a spinner and disables the button while true.
disabledbooleanfalseDisables interaction and reduces opacity.
childrenReactNodeButton label content.
classNamestringExtra CSS classes merged via cn().
react-principles