Skip to content
Caseroom

Components · Actions

Button

An action, or a link that looks like one. Six variants, four sizes.

51% of the 2,062 sites in The UI Index have a button (1,054 sites).

Read the docs

Both previews render the same markup; the HTML tab below is that markup without a framework.

Install

React
npx shadcn@latest add https://www.themes.dev/caseroom/r/button.json
Astro
npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/button.json

Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.

button.tsx
import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
import { buttonVariants, SPINNER_PATH, spinnerClasses, type ButtonVariants } from './button.variants';

type ButtonAsButton = ButtonHTMLAttributes<HTMLButtonElement> & {
  href?: undefined;
  /** Waiting for the action: a spinner, aria-busy, and the button can't be pressed again. */
  loading?: boolean;
};
type ButtonAsLink = AnchorHTMLAttributes<HTMLAnchorElement> & { href: string; disabled?: boolean };
export type ButtonProps = (ButtonAsButton | ButtonAsLink) & ButtonVariants;

/** A <button>, or an <a> when `href` is given. Icon-only buttons (size="icon") need an aria-label. */
export function Button({ variant, size, className, ...props }: ButtonProps) {
  const classes = cn(buttonVariants({ variant, size }), className);
  if (props.href !== undefined) {
    const { disabled, href, ...rest } = props as ButtonAsLink;
    return (
      <a
        href={disabled ? undefined : href}
        aria-disabled={disabled || undefined}
        role={disabled ? 'link' : undefined}
        className={classes}
        {...rest}
      />
    );
  }
  const { type = 'button', loading, disabled, children, ...rest } = props as ButtonAsButton;
  return (
    <button type={type} disabled={disabled || loading} aria-busy={loading ? 'true' : undefined} className={classes} {...rest}>
      {loading && (
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true" className={spinnerClasses}>
          <path d={SPINNER_PATH} />
        </svg>
      )}
      {children}
    </button>
  );
}

export { buttonVariants };
button.variants.ts
import { cva, type VariantProps } from 'class-variance-authority';

export const buttonVariants = cva(
  'inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))]',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-accent active:bg-pressed',
        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed',
        ghost: 'hover:bg-accent hover:text-accent-foreground active:bg-pressed',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-[color-mix(in_oklab,var(--destructive)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--destructive)_var(--press-amount),var(--press-mix-destructive,black))]',
        link: 'h-auto px-0 underline decoration-foreground/30 underline-offset-4 hover:decoration-foreground active:scale-100 active:decoration-2 active:decoration-foreground',
      },
      size: {
        sm: 'h-8 px-3 text-[13px]',
        default: 'h-9 px-4',
        lg: 'h-11 px-5 text-[15px]',
        icon: 'size-9',
      },
    },
    defaultVariants: { variant: 'default', size: 'default' },
  },
);

export type ButtonVariants = VariantProps<typeof buttonVariants>;

/** The spinner of a loading button. It turns slowly; with reduced motion it stands still (the label says it waits). */
export const spinnerClasses = 'animate-spin motion-reduce:animate-none';
export const SPINNER_PATH = 'M8 2a6 6 0 1 0 6 6';