Skip to main content

Button

The Button component is a versatile, accessible button built with class-variance-authority for type-safe variants.

Basic Usage

import { Button } from "@repo/ui";

function Example() {
  return <Button>Click me</Button>;
}

Props

variant
string
default:"default"
The visual style variant of the button.Options: default, destructive, danger, outline, secondary, ghost, link
size
string
default:"default"
The size of the button.Options: default, sm, lg, xl, icon, icon-sm, icon-lg
asChild
boolean
default:"false"
When true, the button will render as a Slot component, merging props with the child element.
isLoading
boolean
default:"false"
When true, displays a loading spinner and disables the button.
disabled
boolean
default:"false"
When true, disables the button.
className
string
Additional CSS classes to apply to the button.

Variants

The default primary button style with solid background.
<Button variant="default">Default Button</Button>

Sizes

<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
  • sm - Height: 32px (h-8)
  • default - Height: 36px (h-9)
  • lg - Height: 40px (h-10)
  • xl - Height: 44px (h-11)

Loading State

Buttons can display a loading spinner:
function Example() {
  const [loading, setLoading] = useState(false);
  
  return (
    <Button 
      isLoading={loading}
      onClick={() => setLoading(true)}
    >
      Save Changes
    </Button>
  );
}
When isLoading is true:
  • A Loader2 spinner icon appears
  • The button is automatically disabled
  • The button text remains visible

Using as Child (asChild)

The asChild prop allows the button to merge its props with a child component:
import { Link } from "react-router-dom";

<Button asChild>
  <Link to="/dashboard">Go to Dashboard</Link>
</Button>
This renders a Link component with button styling, useful for routing libraries.

Complete Example

import { Button } from "@repo/ui";
import { Trash2, Download, Plus } from "lucide-react";

function ActionBar() {
  const [deleting, setDeleting] = useState(false);
  
  return (
    <div className="flex gap-2">
      {/* Primary action */}
      <Button>
        <Plus />
        Create New
      </Button>
      
      {/* Secondary action */}
      <Button variant="outline">
        <Download />
        Export
      </Button>
      
      {/* Destructive action with loading */}
      <Button 
        variant="destructive"
        isLoading={deleting}
        onClick={() => setDeleting(true)}
      >
        <Trash2 />
        Delete
      </Button>
      
      {/* Icon-only button */}
      <Button size="icon" variant="ghost">
        <Plus />
      </Button>
    </div>
  );
}

TypeScript Types

type ButtonProps = React.ComponentProps<"button"> & {
  variant?: "default" | "destructive" | "danger" | "outline" | "secondary" | "ghost" | "link";
  size?: "default" | "sm" | "lg" | "xl" | "icon" | "icon-sm" | "icon-lg";
  asChild?: boolean;
  isLoading?: boolean;
};

Accessibility

  • Supports keyboard navigation (Enter/Space to activate)
  • Proper focus indicators with ring styles
  • Disabled state prevents interaction
  • Loading state automatically disables and announces
  • Works with aria-invalid for form validation

Styling Details

The button uses several advanced Tailwind features:
  • Focus visible rings - focus-visible:ring-ring/50 focus-visible:ring-[3px]
  • SVG sizing - Automatically sizes icons to 16px (size-4)
  • Transitions - Smooth hover and focus transitions
  • Dark mode support - Variant-specific dark mode styles

Build docs developers (and LLMs) love