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).
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.
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 };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';---
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>
)
}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';<div class="flex flex-wrap items-center gap-3">
<button type="button" class="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 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))] h-9 px-4">
Publish
</button>
<button type="button" class="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 bg-secondary text-secondary-foreground hover:bg-accent active:bg-pressed h-9 px-4">
Save draft
</button>
<button type="button" class="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 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4">
Preview
</button>
<button type="button" class="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 hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4">
Cancel
</button>
<button type="button" class="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 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))] h-9 px-4">
Delete
</button>
<a href="#" class="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 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 underline decoration-foreground/30 underline-offset-4 hover:decoration-foreground active:scale-100 active:decoration-2 active:decoration-foreground h-9 px-4">
Read the docs
</a>
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md 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 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-8 px-3 text-[13px]">
Small
</button>
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md 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 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))] h-11 px-5 text-[15px]">
Large
</button>
<button type="button" class="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 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed size-9" aria-label="Add">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M8 3v10M3 8h10"></path></svg>
</button>
<button type="button" disabled aria-busy="true" class="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 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))] h-9 px-4">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class="animate-spin motion-reduce:animate-none"><path d="M8 2a6 6 0 1 0 6 6"></path></svg>
Saving
</button>
<button type="button" disabled class="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 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))] h-9 px-4">
Disabled
</button>
</div>