Use Button for actions such as saving, opening a dialog, or changing state.
For navigation, use an anchor or router link styled with buttonVariants; do not put a link inside
a button.
import { Download, Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function ButtonDemo() {
return (
<div className="grid w-full gap-6">
<div className="flex flex-wrap items-center gap-4">
{(
[
"default",
"outline",
"ghost",
"destructive",
"link",
"noShadow",
"neutral",
"reverse",
] as const
).map((variant) => (
<Button key={variant} variant={variant}>
{variant}
</Button>
))}
</div>
<div className="flex flex-wrap items-center gap-4">
<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button>Default</Button>
<Button size="lg">Large</Button>
<Button size="icon" aria-label="Add item">
<Plus aria-hidden="true" />
</Button>
</div>
<div className="flex flex-wrap items-center gap-4">
<Button>
<Download aria-hidden="true" />
Download
</Button>
<Button disabled>Unavailable</Button>
<Button disabled aria-busy="true">
Saving…
</Button>
</div>
</div>
);
}
#Installation
Complete the base setup. The CLI installs both files. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/button.jsonnpm install @base-ui/react@^1.8.0Create components/ui/button-variants.ts:
import { cva } from "class-variance-authority";
const raisedPress =
"shadow-shadow hover:not-data-disabled:translate-x-pressX hover:not-data-disabled:translate-y-pressY hover:not-data-disabled:shadow-press active:not-data-disabled:translate-x-boxShadowX active:not-data-disabled:translate-y-boxShadowY active:not-data-disabled:shadow-[0_0_0_0_var(--border)]";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-base text-sm font-base transition-[translate,box-shadow,background-color,color] duration-[140ms] ease-[ease] motion-reduce:transition-none gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50",
{
variants: {
variant: {
default: `text-main-foreground bg-main border-2 border-border ${raisedPress}`,
outline: `bg-background text-foreground border-2 border-border ${raisedPress}`,
ghost:
"border-2 border-transparent bg-transparent text-foreground hover:border-border hover:bg-main hover:text-main-foreground",
destructive: `bg-destructive text-destructive-foreground border-2 border-border ${raisedPress}`,
link: "text-foreground underline-offset-4 hover:underline",
noShadow: "text-main-foreground bg-main border-2 border-border",
neutral: `bg-secondary-background text-foreground border-2 border-border ${raisedPress}`,
reverse:
"text-main-foreground bg-main border-2 border-border hover:not-data-disabled:translate-x-reverseBoxShadowX hover:not-data-disabled:translate-y-reverseBoxShadowY hover:not-data-disabled:shadow-shadow active:not-data-disabled:translate-x-0 active:not-data-disabled:translate-y-0 active:not-data-disabled:shadow-[0_0_0_0_var(--border)]",
},
size: {
default: "h-10 px-4 py-2",
xs: "h-7 px-2 text-xs",
sm: "h-9 px-3",
lg: "h-11 px-8",
icon: "size-10",
"icon-xs": "size-7",
"icon-sm": "size-9",
"icon-lg": "size-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export { buttonVariants };
Create components/ui/button.tsx:
"use client";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import type { VariantProps } from "class-variance-authority";
import { buttonVariants } from "@/components/ui/button-variants";
import { cn } from "@/lib/utils";
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={
typeof className === "function"
? (state) => cn(buttonVariants({ variant, size }), className(state))
: cn(buttonVariants({ variant, size }), className)
}
{...props}
/>
);
}
export { Button };
#Usage
import { Button } from "@/components/ui/button";
export default function Example() {
return <Button>Save changes</Button>;
}Start with the normal button above.
Use variant to change the appearance and size to change the dimensions.
Use neutral for a raised action on a neutral surface.
Keyboard focus uses the shared --ring token.
The live preview shows the available styles, icon buttons, disabled states, and loading examples.
#API reference
| Prop / part | Purpose | Default |
|---|---|---|
variant | default, outline, ghost, destructive, link, noShadow, neutral, reverse | default |
size | xs, sm, default, lg, icon, icon-xs, icon-sm, icon-lg | default |
type | button, submit, or reset | Base UI default |
disabled | Prevents interaction | false |
render | Uses another button element while preserving behavior | Native button |
aria-busy | Announces pending work; it does not disable the control | Unset |
#Accessibility
Give every icon-only button an accessible name with aria-label.
Set type="submit" explicitly for form submission.
Enter and Space activate the button.
Manage pending state in your application and disable repeated submissions when appropriate.
A custom element supplied to render must forward its props and ref to the underlying button.
#Navigation and composition
The link variant changes appearance only.
For a destination, import buttonVariants and apply it to an anchor or router link:
import { buttonVariants } from "@/components/ui/button-variants";
<a href="/docs" className={buttonVariants({ variant: "outline" })}>
Read the guide
</a>;