Button
A button component that can be rendered as another tag or focusable when disabled.
import { Button } from "@/components/ui/button";
export function ButtonDemo() {
return <Button>Button</Button>;
}Installation
import { cva, type VariantProps } from "class-variance-authority";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cn } from "@/lib/cn";
const buttonVariants = cva(
[
"inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm outline-none transition-all duration-200",
"focus-visible:ring-3 focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50",
"[&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
],
{
defaultVariants: {
size: "default",
variant: "primary",
},
variants: {
size: {
xs: "h-6 gap-1 rounded-md px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-md px-2 text-[0.8125rem] [&_svg:not([class*='size-'])]:size-3.5",
default: "h-8 gap-1.5 px-2.5",
lg: "h-9 px-3",
xl: "h-10 px-3.5 text-base [&_svg:not([class*='size-'])]:size-4.5",
"icon-xs":
"size-6 rounded-md not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-md not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
icon: "size-8",
"icon-lg": "size-9",
"icon-xl": "size-10 [&_svg:not([class*='size-'])]:size-4.5",
},
variant: {
primary:
"bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-primary/30",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border bg-background text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground focus-visible:ring-accent dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
ghost:
"hover:bg-accent hover:text-accent-foreground focus-visible:ring-accent dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
info: "bg-info text-info-foreground hover:bg-info/90 focus-visible:ring-info/30",
success:
"bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/30",
warning:
"bg-warning text-warning-foreground hover:bg-warning/90 focus-visible:ring-warning/30",
danger: "bg-danger text-white hover:bg-danger/90 focus-visible:ring-danger/30",
"danger-soft": "bg-danger/15 text-danger hover:bg-danger/20 focus-visible:ring-danger/30",
},
},
}
);
type ButtonVariantProps = VariantProps<typeof buttonVariants>;
type ButtonProps = React.ComponentProps<typeof ButtonPrimitive> & ButtonVariantProps;
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
export type { ButtonProps, ButtonVariantProps };Usage
import { Button } from "@/components/ui/button";<Button>Button</Button>Cursor
Tailwind v4 switched from cursor: pointer to cursor: default for the button component. See reference
If you'd like to continue using cursor: pointer by default, add these base styles to your CSS:
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}Examples
Variants
import { Button } from "@/components/ui/button";
export function ButtonDemo() {
return (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2 sm:flex-row">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<Button variant="info">Info</Button>
<Button variant="success">Success</Button>
<Button variant="warning">Warning</Button>
<Button variant="danger">Danger</Button>
<Button variant="danger-soft">Danger Soft</Button>
</div>
</div>
);
}Sizes
import { IconTrash } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
export function ButtonDemo() {
return (
<div className="space-y-3 *:grid *:grid-cols-[1fr_auto_1fr] *:justify-items-start *:gap-3">
<div>
<Button size="xs">Extra small</Button>
<Button size="icon-xs">
<IconTrash />
</Button>
<Button size="xs">
<IconTrash />
Extra small
</Button>
</div>
<div>
<Button size="sm">Small</Button>
<Button size="icon-sm">
<IconTrash />
</Button>
<Button size="sm">
<IconTrash />
Small
</Button>
</div>
<div>
<Button size="default">Default</Button>
<Button size="icon">
<IconTrash />
</Button>
<Button size="default">
<IconTrash />
Default
</Button>
</div>
<div>
<Button size="lg">Large</Button>
<Button size="icon-lg">
<IconTrash />
</Button>
<Button size="lg">
<IconTrash />
Large
</Button>
</div>
<div>
<Button size="xl">Extra large</Button>
<Button size="icon-xl">
<IconTrash />
</Button>
<Button size="xl">
<IconTrash />
Extra large
</Button>
</div>
</div>
);
}Render as another tag
The button can remain keyboard accessible while being rendered as another tag, such as a <div>, by specifying nativeButton={false}
The Button component enforces button semantics. nativeButton={false} signals that the rendered tag is not a <button>, but it must still be a tag that can receive button semantics (role="button", keyboard interaction handlers).
import { Button } from "@/components/ui/button";
export function ButtonDemo() {
return (
<Button nativeButton={false} render={<div />}>
Click me
</Button>
);
}Rendering link as button
The Base UI Button component always applies role="button", which overrides the semantic link role on <a> elements. Use buttonVariants with a plain <a> tag instead.
import { cn } from "@/lib/cn";
import { buttonVariants } from "@/components/ui/button";
export function ButtonDemo() {
return (
<a href="#" className={cn(buttonVariants({ variant: "secondary", size: "lg" }))}>
Login
</a>
);
}Loading state
For buttons that enter a loading state after being clicked, specify the focusableWhenDisabled prop to ensure focus remains on the button when it becomes disabled. This prevents focus from being lost and maintains the tab order.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
export function ButtonDemo() {
const [loading, setLoading] = useState(false);
return (
<Button
disabled={loading}
focusableWhenDisabled
onClick={() => {
setLoading(true);
setTimeout(() => {
setLoading(false);
}, 3000);
}}
>
{loading && <Spinner />}
{loading ? "Submitting" : "Submit"}
</Button>
);
}API Reference
Button props
Type
'primary' | 'outline' | 'ghost' | 'secondary' | 'link' | 'info' | 'success' | 'warning' | 'danger' | 'danger-soft'Default
'primary'Type
'xs' | 'sm' | 'default' | 'lg' | 'xl' | 'icon-xs' | 'icon-sm' | 'icon' | 'icon-lg' | 'icon-xl'Default
'default'