Button

Versatile button with variants, sizes, loading state, and icon support

import { useIcon } from "@/lib/icon-context";
import { Button } from "@/components/ui/button";
export default function ButtonDemo() {
const Plus = useIcon("plus");
const ArrowRight = useIcon("arrow-right");
const Search = useIcon("search");
return (
<div className="flex flex-wrap items-center gap-2">
<Button leadingIcon={Plus}>Create</Button>
<Button variant="secondary" trailingIcon={ArrowRight}>Next</Button>
<Button variant="tertiary" leadingIcon={Search} trailingIcon={ArrowRight}>
Search
</Button>
</div>
);
}

Installation

npx shadcn@latest add https://www.deltacomponents.dev/r/button.json

Usage

1import { Button } from "@/components/ui/button";
2
3<Button>Button</Button>

Basic

import { Button } from "@/components/ui/button";
export default function ButtonBasic() {
return <Button>Button</Button>;
}

Playground

import { Button } from "@/components/ui/button";
<Button>Get started</Button>

Variants

import { Button } from "@/components/ui/button";
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="ghost">Ghost</Button>
</div>
);
}

Icon only

An icon-* size makes a square, one step for each text size, with icon-xl the 48px step past shadcn's ladder. With no text the aria-label is the accessible name, so it says what a press will do, not what the button shows.

import { useState } from "react";
import { useIcon } from "@/lib/icon-context";
import { Button } from "@/components/ui/button";
type IconSize = "icon-sm" | "icon" | "icon-lg" | "icon-xl";
export default function ButtonIcon() {
const Play = useIcon("play");
const Pause = useIcon("pause");
const [playing, setPlaying] = useState(false);
const [size, setSize] = useState<IconSize>("icon-xl");
return (
<div className="flex w-fit flex-col items-center gap-5">
{/* No visible text, so the label is the accessible name, and it names
the action the press will take, not the state the button shows. */}
<Button
size={size}
aria-label={playing ? "Pause" : "Play"}
aria-pressed={playing}
onClick={() => setPlaying((value) => !value)}
>
{/* A transport control is a filled shape: the glyph is the thing
itself, not an outline of it. */}
{playing ? <Pause className="fill-current" /> : <Play className="fill-current" />}
</Button>
<label className="text-caption text-muted-foreground flex items-center gap-2 self-start">
Size
<select
value={size}
onChange={(e) => setSize(e.target.value as IconSize)}
className="border-border bg-background text-foreground text-caption rounded-md border px-2 py-1 outline-none focus-visible:ring-1 focus-visible:ring-[color:var(--focus-ring,#6B97FF)]"
>
<option value="icon-sm">icon-sm</option>
<option value="icon">icon</option>
<option value="icon-lg">icon-lg</option>
<option value="icon-xl">icon-xl</option>
</select>
</label>

Loading & Disabled

import { useState } from "react";
import { useIcon } from "@/lib/icon-context";
import { Button } from "@/components/ui/button";
export default function ButtonLoading() {
const Loader = useIcon("loader");
const [loading, setLoading] = useState(false);
return (
<div className="flex flex-wrap items-center gap-2">
<Button
loading={loading}
onClick={() => {
setLoading(true);
setTimeout(() => setLoading(false), 2000);
}}
>
{loading ? "Loading" : "Click me"}
</Button>
<Button variant="secondary" loading leadingIcon={Loader}>
Saving
</Button>
<Button disabled>Disabled</Button>
</div>
);
}

API Reference