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
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.
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(--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';---
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(--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';<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(--press-mix-destructive,black))] 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>