Button
A button or a component that looks like a button. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button>Button</Button>;
}
Installation
pnpm dlx shadcn@latest add @cnippet/button
Usage
import { Button } from "@/components/ui/button";<Button>Button</Button>Link
You can use the render prop to make another component look like a button. Here's an example of a link that looks like a button.
import Link from "next/link";
import { Button } from "@/components/ui/button";
export function LinkAsButton() {
return <Button render={<Link href="/login" />}>Login</Button>;
}Examples
Default
The standard filled button using the primary brand color. Use this as the primary call-to-action on any surface.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button>Button</Button>;
}
Outline
A bordered button with a transparent background. Use it for secondary actions that should be visible but not dominant.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button variant="outline">Outline</Button>;
}
Secondary
A lower-contrast filled button for supplementary actions that sit alongside a primary button.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button variant="secondary">Secondary</Button>;
}
Destructive
A red-tinted button that signals an irreversible or dangerous action such as deletion.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button variant="destructive-outline">Delete</Button>;
}
Destructive Outline
The destructive intent expressed as an outlined button — less visually heavy while still clearly communicating danger.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button variant="destructive-outline">Delete</Button>;
}
Ghost
A button with no border or background until hovered. Ideal for low-priority actions inside dense UIs like toolbars.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button variant="ghost">Ghost</Button>;
}
Link
Styled as a plain text link but retains button semantics and keyboard behavior.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button variant="link">Link</Button>;
}
Extra-small Size
The xs size targets tight spaces such as table rows or inline callouts.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button size="xs">Button</Button>;
}
Small Size
The sm size is suitable for secondary action areas where space is limited.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button size="sm">Button</Button>;
}
Large Size
The lg size works well as a prominent hero or marketing CTA.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button size="lg">Button</Button>;
}
Extra-large Size
The xl size creates a bold, full-width call-to-action for landing pages or onboarding flows.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button size="xl">Button</Button>;
}
Disabled
Pass disabled to prevent interaction. The button remains visible but is styled at reduced opacity.
import { Button } from "@/components/ui/button";
export default function Particle() {
return <Button disabled>Button</Button>;
}
Icon
A square icon-only button. Always add aria-label when there is no visible text label.
import { SearchIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
export function Pattern() {
return (
<Button aria-label="Search" size="icon">
<SearchIcon aria-hidden="true" />
</Button>
);
}
With Trailing Icon
An icon placed after the label reinforces the action direction — commonly used for navigation or expand patterns.
import { ArrowRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
export function Pattern() {
return (
<Button>
Get Started
<ArrowRightIcon aria-hidden="true" />
</Button>
);
}
With Leading Icon
A leading icon provides quick visual identification of the action before the user reads the label.
import { CloudDownloadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
export function Pattern() {
return (
<Button>
<CloudDownloadIcon aria-hidden="true" />
Download
</Button>
);
}
Outline With Icon
Combines the outline style with a leading icon, suitable for secondary actions that benefit from visual cues.
import { PlusIcon } from "lucide-react";
import { Button } from "@/registry/default/ui//button";
export function Pattern() {
return (
<Button variant="outline">
<PlusIcon aria-hidden="true" />
Add Item
</Button>
);
}
Ghost With Icon
A ghost button with a trailing icon — common for expand, link, or navigation affordances in compact UIs.
import { LogOutIcon } from "lucide-react";
import { Button } from "@/registry/default/ui//button";
export function Pattern() {
return (
<Button variant="ghost">
Logout
<LogOutIcon aria-hidden="true" />
</Button>
);
}
Destructive With Icon
A delete or remove action reinforced with a warning icon to ensure the user notices the destructive intent.
import { Trash2Icon } from "lucide-react";
import { Button } from "@/registry/default/ui//button";
export function Pattern() {
return (
<Button variant="destructive">
<Trash2Icon aria-hidden="true" />
Delete Account
</Button>
);
}
Link With Icon
Combines the link style with a trailing arrow or external-link icon to indicate navigation away from the current page.
import { ArrowUpRightIcon } from "lucide-react";
import { Button } from "@/registry/default/ui//button";
export function Pattern() {
return (
<Button className="group/link-button" variant="link">
View Documentation
<ArrowUpRightIcon
aria-hidden="true"
className="transition-transform group-hover/link-button:rotate-45"
/>
</Button>
);
}
Star button with count
A GitHub-style star button that combines an icon, label, and a separated count badge using a vertical border divider.
import { StarIcon } from "lucide-react";
import { Button } from "@/registry/default/ui//button";
export function Pattern() {
return (
<Button className="pe-0" variant="outline">
<StarIcon aria-hidden="true" />
Star
<span className="relative ms-1 px-2 font-medium text-muted-foreground text-xs before:absolute before:inset-0 before:left-0 before:w-px before:bg-border">
589
</span>
</Button>
);
}
With Unread Badge
An inbox or notification button with an absolute-positioned badge showing the unread count, positioned at the top-right corner.
import { MailIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
export function Pattern() {
return (
<Button
aria-label="Inbox (8 unread)"
className="relative gap-2"
variant="outline"
>
<MailIcon aria-hidden="true" />
Inbox
<Badge
aria-hidden="true"
className="absolute -top-1.5 -right-2 rounded-full px-1"
size="sm"
variant="destructive"
>
8
</Badge>
</Button>
);
}
With Shortcut
Pairs a label with Kbd shortcut hints so users can discover keyboard equivalents at a glance.
import { SearchIcon } from "lucide-react";
import { Button } from "@/registry/default/ui//button";
import { Kbd, KbdGroup } from "@/registry/default/ui//kbd";
export function Pattern() {
return (
<Button aria-label="Search (Command K)" variant="outline">
<SearchIcon aria-hidden="true" />
<span>Search</span>
<KbdGroup aria-hidden="true">
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</Button>
);
}
Copy With Feedback
Clicking the copy icon triggers a brief state change — the icon swaps to a check mark for 2 seconds to confirm the copy without a toast.
"use client";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { Button } from "@/registry/default/ui//button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/default/ui//tooltip";
export function Pattern() {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1500 });
return (
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label={isCopied ? "Copied" : "Copy"}
onClick={() => copyToClipboard("https://ui.cnippet.dev")}
size="icon"
variant="outline"
>
{isCopied ? (
<CheckIcon aria-hidden="true" />
) : (
<CopyIcon aria-hidden="true" />
)}
</Button>
}
/>
<TooltipContent>{isCopied ? "Copied" : "Copy link"}</TooltipContent>
</Tooltip>
);
}
Hamburger Toggle
A menu toggle that morphs its three lines into an ✕ on press, providing smooth visual feedback for sidebar open/close state.
"use client";
import { MenuIcon, XIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/default/ui//button";
export function Pattern() {
const [open, setOpen] = useState(false);
return (
<Button
aria-expanded={open}
aria-label={open ? "Close menu" : "Open menu"}
onClick={() => setOpen((v) => !v)}
size="icon"
variant="outline"
>
<span className="relative flex size-4 items-center justify-center">
<MenuIcon
aria-hidden="true"
className={cn(
"absolute size-4 transition-all duration-200",
open
? "rotate-90 scale-75 opacity-0"
: "rotate-0 scale-100 opacity-100",
)}
/>
<XIcon
aria-hidden="true"
className={cn(
"absolute size-4 transition-all duration-200",
open
? "rotate-0 scale-100 opacity-100"
: "-rotate-90 scale-75 opacity-0",
)}
/>
</span>
</Button>
);
}
Sliding Icon Reveal
The icon is hidden off-screen and slides into view on hover using a CSS transform, creating an animated "reveal" effect on the button.
import { ArrowRightIcon } from "lucide-react";
import { Button } from "@/registry/default/ui//button";
export function Pattern() {
return (
<Button className="group/sliding relative overflow-hidden rounded-full px-6">
<span className="inline-flex items-center transition-transform duration-300 group-hover/sliding:-translate-x-2">
Get Started
</span>
<ArrowRightIcon
aria-hidden="true"
className="absolute right-2.5 translate-x-8 opacity-0 transition-all duration-300 group-hover/sliding:translate-x-0 group-hover/sliding:opacity-100"
/>
</Button>
);
}
Social Login
A set of OAuth login buttons for GitHub and Google alongside an email option, separated by an "or continue with" divider row.
import { GitBranch } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
function GoogleIcon({ className }: { className?: string }) {
return (
<svg
aria-hidden="true"
className={className}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
);
}
export function Pattern() {
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<Button className="w-full" variant="outline">
<GitBranch className="size-4" />
Continue with GitHub
</Button>
<Button className="w-full" variant="outline">
<GoogleIcon className="size-4" />
Continue with Google
</Button>
<div className="flex items-center gap-3">
<Separator className="flex-1" />
<span className="text-muted-foreground text-xs">or</span>
<Separator className="flex-1" />
</div>
<Button className="w-full">Continue with email</Button>
</div>
);
}
Loading and Success State
A single button that cycles through idle → loading → success states with an animated icon swap, useful for async form submissions.
"use client";
import { CheckIcon, CloudUploadIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
type State = "idle" | "loading" | "success";
export function Pattern() {
const [state, setState] = useState<State>("idle");
const handleClick = () => {
if (state !== "idle") return;
setState("loading");
setTimeout(() => {
setState("success");
setTimeout(() => setState("idle"), 2000);
}, 1800);
};
return (
<Button
disabled={state === "loading"}
loading={state === "loading"}
onClick={handleClick}
variant={state === "success" ? "outline" : "default"}
>
{state === "success" ? (
<>
<CheckIcon className="size-4 text-emerald-500" />
Saved
</>
) : (
<>
<CloudUploadIcon className="size-4" />
{state === "loading" ? "Saving…" : "Save Changes"}
</>
)}
</Button>
);
}
File Upload
Clicking the button triggers a hidden file input; selected filenames appear as a pill with a clear button to reset the selection.
"use client";
import { PaperclipIcon, XIcon } from "lucide-react";
import { useRef, useState } from "react";
import { Button } from "@/components/ui/button";
export function Pattern() {
const inputRef = useRef<HTMLInputElement>(null);
const [fileName, setFileName] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
setFileName(file?.name ?? null);
};
const handleClear = () => {
setFileName(null);
if (inputRef.current) inputRef.current.value = "";
};
return (
<div className="flex flex-col items-start gap-3">
<input
accept="image/*,.pdf,.doc,.docx"
className="sr-only"
id="file-upload"
onChange={handleChange}
ref={inputRef}
type="file"
/>
<Button onClick={() => inputRef.current?.click()} variant="outline">
<PaperclipIcon className="size-4" />
{fileName ? "Change file" : "Attach file"}
</Button>
{fileName && (
<div className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-1.5 text-sm">
<PaperclipIcon className="size-3.5 shrink-0 text-muted-foreground" />
<span className="max-w-45 truncate">{fileName}</span>
<button
aria-label="Remove file"
className="ml-1 text-muted-foreground hover:text-foreground"
onClick={handleClear}
type="button"
>
<XIcon className="size-3.5" />
</button>
</div>
)}
</div>
);
}
Split Button
A Group with a primary action button and a GroupSeparator followed by a chevron trigger that opens a Menu of additional options.
import { ChevronDownIcon, RocketIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export function Pattern() {
return (
<Group aria-label="Deploy options">
<Button>
<RocketIcon className="size-4" />
Deploy
</Button>
<GroupSeparator />
<Menu>
<MenuTrigger
render={
<Button
aria-label="More deploy options"
className="px-2 py-3.75"
size="icon-sm"
/>
}
>
<ChevronDownIcon className="size-5" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>Deploy to Staging</MenuItem>
<MenuItem>Deploy to Preview</MenuItem>
<MenuItem>Deploy with custom env</MenuItem>
<MenuItem variant="destructive">Force deploy</MenuItem>
</MenuPopup>
</Menu>
</Group>
);
}
Segmented View Toggle
A Group of three icon buttons (List, Table, Kanban) that act as a view mode selector, highlighting the active choice with a filled style.
Viewing as: list
"use client";
import { KanbanIcon, LayoutListIcon, TableIcon } from "lucide-react";
import { Fragment, useState } from "react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
type View = "list" | "table" | "board";
const views: { id: View; icon: typeof LayoutListIcon; label: string }[] = [
{ icon: LayoutListIcon, id: "list", label: "List" },
{ icon: TableIcon, id: "table", label: "Table" },
{ icon: KanbanIcon, id: "board", label: "Board" },
];
export function Pattern() {
const [view, setView] = useState<View>("list");
return (
<div className="flex flex-col items-center gap-4">
<Group aria-label="View mode">
{views.map((v, i) => (
<Fragment key={v.id}>
{i > 0 && <GroupSeparator />}
<Button
aria-pressed={view === v.id}
onClick={() => setView(v.id)}
size="sm"
variant={view === v.id ? "default" : "ghost"}
>
<v.icon className="size-4" />
{v.label}
</Button>
</Fragment>
))}
</Group>
<p className="text-muted-foreground text-sm">
Viewing as:{" "}
<span className="font-medium text-foreground capitalize">{view}</span>
</p>
</div>
);
}
On This Page

