Skip to content
    Style: Wireframe

    Components · Actions

    Button

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

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

    Used in UI Pro: Footer, Hero, Call to action, Pricing, Split, Logo wall, Form section, Newsletter section, Header, Navigation, Breadcrumb, Comparison, Content card, Search, Dialog, Sidebar navigation, Pagination, Carousel, Data table

    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/ui/r/button.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/button.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI 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(--press-mix-destructive,black))] 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';