Button with default, outline, secondary, ghost, destructive and link variants, sm, default and lg sizes plus icon sizes; renders as a link or another element with `render`.
Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/button.jsonInstalls to @/components/ui/button.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1Copy the source into your project:
import { Button as ButtonPrimitive } from '@base-ui/react/button'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring cursor-pointer disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 data-disabled:cursor-not-allowed data-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
// Colours kept from the previous Rapid button (ghost is shadcn's), with a pressed background instead of
// shadcn's press-down nudge; sizes, focus and states are shadcn's
default: 'bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80',
outline:
'border-input bg-background text-foreground hover:bg-accent active:bg-[color-mix(in_oklch,var(--accent),var(--foreground)_6%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--secondary-foreground)_8%)] active:bg-[color-mix(in_oklch,var(--secondary),var(--secondary-foreground)_16%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
'hover:bg-muted hover:text-foreground active:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_10%)] aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive: 'bg-destructive text-white hover:bg-destructive/90 active:bg-destructive/80',
// Quiet at rest so a delete sits at the same weight as the actions beside it; the destructive colour arrives on
// hover and focus, where the intent is real
'ghost-destructive':
'hover:bg-destructive/10 hover:text-[color-mix(in_oklab,var(--destructive)_80%,black)] focus-visible:text-[color-mix(in_oklab,var(--destructive)_80%,black)] active:bg-destructive/20 aria-expanded:bg-destructive/10 aria-expanded:text-destructive dark:hover:bg-destructive/20 dark:hover:text-[color-mix(in_oklab,var(--destructive)_50%,white)] dark:focus-visible:text-[color-mix(in_oklab,var(--destructive)_50%,white)]',
link: 'text-primary underline underline-offset-4',
},
size: {
default: 'h-10 gap-2 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
xs: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 rounded-[min(var(--radius-md),12px)] px-3 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-12 gap-2 px-6 has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5',
icon: 'size-10',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-8 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-12',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
// Base UI's Button defaults to type="button" on a native <button>, so buttons never submit a form by accident.
// Render a link with `render={<a href="…" />}` and `nativeButton={false}`. Base UI would give it role="button"; we drop
// that role for <a> elements so a link keeps its link semantics.
function Button({
className,
variant = 'default',
size = 'default',
render,
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
const isLink = React.isValidElement(render) && render.type === 'a' && !('role' in props)
return (
<ButtonPrimitive
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
render={render}
{...(isLink ? { role: undefined } : null)}
{...props}
/>
)
}
export { Button, buttonVariants }A versatile button component that supports multiple variants, sizes, and states. Built on the Base UI Button and styled with Tailwind CSS using class-variance-authority for dynamic variants.
Features
- Seven visual variants:
default,outline,secondary,ghost,destructive,ghost-destructive,link - Text sizes (
xs,sm,default,lg) and square icon sizes (icon-xs,icon-sm,icon,icon-lg) - Icon spacing handled automatically via
data-icon="inline-start"/data-icon="inline-end" - Polymorphic composition with the
renderprop (e.g. render a link) - Exposes
data-slot="button",data-variantanddata-sizefor styling hooks - Integrates with
ButtonGroup(rounded corners adapt inside a group) - Full keyboard navigation and visible focus ring
aria-invalidstyling for form validation
Usage
import { Button } from '@/components/ui/button'
import { ArrowRight, Download, Printer } from 'lucide-react'
export function DecisionActions() {
return (
<div className="flex flex-wrap gap-2">
{/* Primary action */}
<Button>Send application</Button>
{/* With variant and size */}
<Button variant="outline" size="lg">
Save draft
</Button>
{/* With icons */}
<Button variant="secondary">
<Download data-icon="inline-start" />
Download decision
</Button>
<Button variant="outline">
Continue
<ArrowRight data-icon="inline-end" />
</Button>
{/* Icon-only */}
<Button size="icon" variant="ghost" aria-label="Print decision">
<Printer />
</Button>
{/* As link */}
<Button render={<a href="/my-pages" />} nativeButton={false}>
Go to My pages
</Button>
</div>
)
}Examples
Default Button
The default button style with primary color, for the main action on a page.
All Variants
Each variant with the kind of action it suits.
Disabled
Disabled buttons in the footer of an application that isn’t signed yet. Explain nearby why the action isn’t available.
Sizes
Text button sizes, from xs to lg.
Icon Sizes
Square sizes for icon-only buttons, and a toolbar of ghost icon buttons. Always provide an aria-label.
Row Actions
ghost-destructive is for a delete action that sits beside other actions. It is as quiet as ghost at rest, so it
doesn't draw the eye in every row, and turns destructive on hover and keyboard focus. Name each icon button with
aria-label, including what it acts on. The label and icon carry the meaning, not the red, and a delete that can't be
undone should still ask for confirmation, for example in an AlertDialog.
<Button size="icon-sm" variant="ghost" aria-label="Edit case 2026-04812">
<PencilIcon />
</Button>
<Button size="icon-sm" variant="ghost" aria-label="Duplicate case 2026-04812">
<CopyIcon />
</Button>
<Button size="icon-sm" variant="ghost-destructive" aria-label="Delete case 2026-04812">
<Trash2Icon />
</Button>With Icons
Mark icons with data-icon="inline-start" or data-icon="inline-end" so the button adjusts its padding. SVGs are sized
automatically unless you pass a size-* class.
Loading State
There is no built-in loading prop. Compose a spinning icon and disable the button while the action runs.
<Button disabled>
<Loader2 data-icon="inline-start" className="animate-spin" />
Sending application…
</Button>As Link
Use the render prop to render the button as another element, such as a link. Set nativeButton={false} whenever the
rendered element is not a <button>, so the button doesn't add button semantics to it.
<Button render={<a href="/my-pages" />} nativeButton={false}>
Go to My pages
</Button>Form Actions
A typical footer in a multi-step e-service: a ghost Back button on the left, and Save draft and Continue on the right.
Props
variant-'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'ghost-destructive' | 'link'(default'default')size-'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'(default'default')render-React.ReactElement | (props, state) => React.ReactElement, the element to render instead of<button>nativeButton-boolean(defaulttrue), set tofalsewhenrenderis not a<button>focusableWhenDisabled-boolean(defaultfalse), keeps a disabled button in the tab order
All native button attributes are forwarded. type defaults to "button" so a button never submits a form by
accident; set type="submit" on the submit button. buttonVariants is also exported for styling other elements as buttons.
Variants
default- Primary action, high emphasisoutline- Secondary actions, medium emphasis, borderedsecondary- Alternative filled style with lower emphasisghost- Minimal emphasis, transparent backgrounddestructive- Dangerous or destructive actions (solid red)ghost-destructive- Transparent likeghost, red on hover and focus; for a delete beside other actionslink- Appears as a text link
Sizes
xs- Compact, with smaller text and padding (32px)sm- Compact size for tight spaces (32px)default- Standard size for most use cases (40px)lg- Large size for high emphasis (48px)icon-sm(32px),icon(40px),icon-lg(48px) - Square formats for icon-only buttonsicon-xs(24px) - For small icon buttons inside fields and chips
No button is shorter than 32px except icon-xs, which keeps a 24px clickable area.
Accessibility
The Button component follows WAI-ARIA guidelines:
- Uses native
buttonelement by default - Supports keyboard navigation
- Maintains focus states
- Communicates disabled states to assistive technology
- Links rendered with
renderandnativeButton={false}keep their link semantics
Best Practices
-
Use appropriate variants for different actions:
defaultfor primary actionsdestructivefor dangerous operationsghost-destructivefor a delete in a row or toolbar of quieter actionsoutline,secondaryorghostfor secondary actions
-
Include descriptive text:
- Use clear, action-oriented labels
- Avoid generic text like "Click here"
- Keep text concise but meaningful
-
Icon usage:
- Use icons to reinforce meaning
- Mark leading/trailing icons with
data-icon - Provide an
aria-labelfor icon-only buttons
-
Loading states:
- Show a spinning icon for async actions
- Disable the button while loading
- Keep the label so the button width stays stable
-
Accessibility:
- Ensure sufficient color contrast
- Maintain focus visibility
Related Components
- ButtonGroup
- Toggle
- Spinner