ButtonNew
pnpm dlx shadcn@latest add …/r/button.jsonAn accessible React button with six variants, three sizes, icon support and a spring press, built on a native button.
import { Button } from "@/components/ui/button"
export function Actions() {
return <Button>Continue</Button>
}import { Button } from "@/components/ui/button";
export default function ButtonDefault() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button>Continue</Button>
<Button variant="outline">Cancel</Button>
</div>
);
}Installation
Install the dependencies.
$ pnpm add class-variance-authority radix-uiAdd the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.
:root { --size-control-sm: 2rem; --size-control-md: 2.25rem; --size-control-lg: 2.5rem; --size-icon-sm: 0.875rem; --size-icon-md: 1rem; --size-icon-lg: 1.25rem; --duration-instant: 80ms; --duration-fast: 150ms; --duration-base: 220ms; --duration-slow: 350ms; --duration-slower: 500ms; --z-popover: 1400; --focus-ring-width: 2px; --focus-ring-offset: 2px; --opacity-disabled: 0.5; --scale-press: 0.97; --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1); --destructive-foreground: oklch(1 0 0); } .dark { --destructive-foreground: oklch(1 0 0); } @theme inline { --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both; --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both; --color-destructive-foreground: var(--destructive-foreground); @keyframes popover-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } } @keyframes popover-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } } } @media (prefers-reduced-motion: reduce) { :root { --duration-instant: 1ms; --duration-fast: 1ms; --duration-base: 1ms; --duration-slow: 1ms; --duration-slower: 1ms; } }Copy the source into your project.
import { cn } from "@/lib/utils"; import { cva, type VariantProps } from "class-variance-authority"; import { Slot } from "radix-ui"; import type { ComponentProps } from "react"; const buttonVariants = cva( [ "inline-flex shrink-0 items-center justify-center gap-2 rounded-lg text-sm font-medium whitespace-nowrap", "transition duration-(--duration-fast) ease-out", // Press: a quick scale on a spring curve. Plain CSS, so it works on the server and with asChild. "active:scale-(--scale-press) active:ease-(--ease-spring)", "outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)", "disabled:pointer-events-none disabled:opacity-(--opacity-disabled)", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--size-icon-md)", ], { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground", destructive: "bg-destructive text-destructive-foreground shadow-xs outline-destructive hover:bg-destructive/90", link: "text-primary underline-offset-4 hover:underline", }, size: { sm: "h-(--size-control-sm) gap-1.5 rounded-md px-3", md: "h-(--size-control-md) px-4", lg: "h-(--size-control-lg) px-5", icon: "size-(--size-control-md)", "icon-sm": "size-(--size-control-sm) rounded-md", "icon-lg": "size-(--size-control-lg)", }, }, defaultVariants: { variant: "default", size: "md" }, }, ); export type ButtonProps = ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { /** Render the single child instead of a button, for example a link, keeping the button styles */ asChild?: boolean; }; /** * A button for actions. It renders a native `button` (type "button" unless you set one), or with * `asChild` its single child, so a link can look like a button without nesting interactive elements. */ function Button({ className, variant, size, asChild = false, type, ...props }: ButtonProps) { const Comp = asChild ? Slot.Root : "button"; return ( <Comp data-slot="button" data-variant={variant ?? "default"} data-size={size ?? "md"} type={asChild ? undefined : (type ?? "button")} className={cn(buttonVariants({ variant, size }), className)} {...props} /> ); } export { Button, buttonVariants };Update the import paths to match your project.
Examples
Variants
Default for the main action, secondary and outline for the rest, destructive for actions that remove things.
import { Button } from "@/components/ui/button";
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<Button variant="link">Link</Button>
</div>
);
}Sizes
Three heights, plus square icon buttons. Icon-only buttons need an aria-label.
import { Button } from "@/components/ui/button";
import { PlusIcon } from "lucide-react";
export default function ButtonSizes() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button size="sm">Small</Button>
<Button>Medium</Button>
<Button size="lg">Large</Button>
<Button size="icon-sm" variant="outline" aria-label="Add item">
<PlusIcon />
</Button>
<Button size="icon" variant="outline" aria-label="Add item">
<PlusIcon />
</Button>
<Button size="icon-lg" variant="outline" aria-label="Add item">
<PlusIcon />
</Button>
</div>
);
}With an icon
Icons sit before or after the label and size themselves.
import { Button } from "@/components/ui/button";
import { ArrowRightIcon, DownloadIcon } from "lucide-react";
export default function ButtonWithIcon() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button variant="outline">
<DownloadIcon aria-hidden />
Download
</Button>
<Button>
Next step
<ArrowRightIcon aria-hidden />
</Button>
</div>
);
}As a link
With asChild, the button styles go onto its child, so a link stays a real link.
import { Button } from "@/components/ui/button";
import { ArrowUpRightIcon } from "lucide-react";
export default function ButtonAsLink() {
return (
<Button asChild variant="outline">
<a href="https://github.com/FewTone/ui" target="_blank" rel="noreferrer">
View on GitHub
<ArrowUpRightIcon aria-hidden />
</a>
</Button>
);
}API reference
Also accepts every native button prop. Exports buttonVariants for styling other elements.
| Prop | Type | Default | Description |
|---|---|---|---|
variant? | "default" | "secondary" | "outline" | "ghost" | "destructive" | "link" | "default" | The visual style. |
size? | "sm" | "md" | "lg" | "icon" | "icon-sm" | "icon-lg" | "md" | Height and padding. Icon sizes are square. |
asChild? | boolean | false | Render the single child (for example a link) with the button styles instead of a button. |
type? | "button" | "submit" | "reset" | "button" | The native button type. Defaults to button so it never submits a form by accident. |
Related components
- InputNew
An accessible React text input with three sizes and clear invalid, disabled and file states, built on a native input.
- SelectNew
An accessible React select with a gliding highlight, groups, labels and a scrollable list, built on Radix Select.
- SwitchNew
An accessible React switch for on/off settings, with a spring-animated thumb, built on Radix Switch.