# Button (Caseroom component)

An action, or a link that looks like one. Six variants, four sizes. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

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

## 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`
- HTML: https://www.themes.dev/caseroom/r/html/button.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/button/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### button.astro

```astro
---
import type { HTMLAttributes } from 'astro/types';
import { cn } from '@/lib/utils';
import { buttonVariants, SPINNER_PATH, spinnerClasses, type ButtonVariants } from './button.variants';

type Props = Omit<HTMLAttributes<'button'>, 'type'> &
  ButtonVariants & {
    href?: string;
    type?: 'button' | 'submit' | 'reset';
    target?: string;
    rel?: string;
    /** Waiting for the action: a spinner, aria-busy, and the button can't be pressed again. */
    loading?: boolean;
  };

const { variant, size, href, type = 'button', disabled, loading, class: className, ...rest } = Astro.props;
const classes = cn(buttonVariants({ variant, size }), className);
---

{
  href !== undefined ? (
    <a href={disabled ? undefined : href} aria-disabled={disabled ? 'true' : undefined} role={disabled ? 'link' : undefined} class={classes} {...(rest as Record<string, unknown>)}>
      <slot />
    </a>
  ) : (
    <button type={type} disabled={disabled || loading} aria-busy={loading ? 'true' : undefined} class={classes} {...rest}>
      {loading && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class={spinnerClasses}><path d={SPINNER_PATH} /></svg>}
      <slot />
    </button>
  )
}
```

### button.tsx

```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

```tsx
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';
```
