Skip to main content

Overview

The Button component is a versatile, accessible button with support for different visual styles, sizes, loading states, and icons. Built with TypeScript and fully typed for excellent developer experience.

TypeScript Types

ButtonVariant

type ButtonVariant = 'primary' | 'secondary' | 'accent' | 'destructive' | 'ghost' | 'outline';

ButtonSize

type ButtonSize = 'sm' | 'md' | 'lg';

ButtonProps

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;
  size?: ButtonSize;
  isLoading?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

Props

variant
ButtonVariant
default:"primary"
The visual style of the button. Available options:
  • primary - Primary action with accent color and shadow
  • secondary - Secondary action with muted styling
  • accent - Highlighted action with accent color
  • destructive - Dangerous or delete actions with red styling
  • ghost - Transparent background, visible on hover
  • outline - Transparent with border, fills on hover
size
ButtonSize
default:"md"
The size of the button:
  • sm - Small (px-3 py-1.5 text-sm)
  • md - Medium (px-4 py-2 text-base)
  • lg - Large (px-6 py-3 text-lg)
isLoading
boolean
default:"false"
When true, displays a loading spinner and disables the button. The button text remains visible while loading.
leftIcon
React.ReactNode
Icon or element to display on the left side of the button text. Hidden when isLoading is true.
rightIcon
React.ReactNode
Icon or element to display on the right side of the button text. Hidden when isLoading is true.
disabled
boolean
default:"false"
When true, disables the button and reduces opacity. Also disabled automatically when isLoading is true.
className
string
Additional CSS classes to apply to the button element.
The Button component extends all standard HTML button attributes via ButtonHTMLAttributes<HTMLButtonElement>, so you can use props like onClick, type, form, etc.

Variant Examples

<Button variant="primary">Primary Button</Button>
The primary variant features:
  • Accent background color (bg-primary)
  • White text (text-primary-foreground)
  • Hover state with darker background
  • Focus ring with primary color
  • Subtle shadow that increases on hover

Size Examples

<Button size="sm">Small Button</Button>
<Button size="md">Medium Button</Button>
<Button size="lg">Large Button</Button>
Size affects padding, font size, and icon gap:
  • Small: px-3 py-1.5 text-sm gap-1.5
  • Medium: px-4 py-2 text-base gap-2
  • Large: px-6 py-3 text-lg gap-2.5

Loading State

import { Button } from '@/components/Button';
import { useState } from 'react';

function SubmitButton() {
  const [isLoading, setIsLoading] = useState(false);
  
  const handleSubmit = async () => {
    setIsLoading(true);
    try {
      await submitForm();
    } finally {
      setIsLoading(false);
    }
  };
  
  return (
    <Button isLoading={isLoading} onClick={handleSubmit}>
      Submit Form
    </Button>
  );
}
When isLoading is true:
  • An animated spinning icon appears
  • The button is automatically disabled
  • Left and right icons are hidden
  • Button text remains visible

Icon Usage

Left Icon

import { ArrowLeft } from 'lucide-react';

<Button leftIcon={<ArrowLeft size={16} />}>
  Go Back
</Button>

Right Icon

import { ArrowRight } from 'lucide-react';

<Button rightIcon={<ArrowRight size={16} />}>
  Continue
</Button>

Icon Only

import { Search } from 'lucide-react';

<Button variant="ghost" leftIcon={<Search size={18} />}>
  {/* Empty children for icon-only button */}
</Button>
Icons are wrapped in a flex-shrink-0 span to prevent them from shrinking when the button text is long.

Complete Example

import { Button } from '@/components/Button';
import { ArrowRight } from 'lucide-react';

function ButtonExample() {
  return (
    <div className="flex flex-wrap gap-3">
      {/* Variants */}
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="accent">Accent</Button>
      <Button variant="destructive">Delete</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="outline">Outline</Button>
      
      {/* Sizes */}
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
      
      {/* States */}
      <Button isLoading>Loading</Button>
      <Button disabled>Disabled</Button>
      
      {/* With Icons */}
      <Button leftIcon={<ArrowRight size={16} />}>
        With Icon
      </Button>
    </div>
  );
}

Accessibility

  • Uses semantic <button> element
  • Supports keyboard navigation (Enter and Space keys)
  • Proper focus management with visible focus ring
  • Disabled state prevents interaction and is announced by screen readers
  • Loading state announced via disabled attribute

Best Practices

  • Use primary for the main call-to-action
  • Use secondary for supporting actions
  • Use destructive only for dangerous or irreversible actions
  • Use ghost or outline for tertiary actions
Button text should clearly describe the action that will occur when clicked. Avoid vague labels like “Click here” or “Submit”.
Always use isLoading during async operations to prevent duplicate submissions and provide visual feedback.
Keep icon sizes consistent across your application:
  • Small buttons: 14-16px icons
  • Medium buttons: 16-18px icons
  • Large buttons: 18-20px icons

Build docs developers (and LLMs) love