Spinner
An indicator that can be used to show a loading state. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { Spinner } from "@/components/ui/spinner";
export default function Particle() {
return <Spinner />;
}
Installation
pnpm dlx shadcn@latest add @cnippet/spinner
Usage
import { Spinner } from "@/components/ui/spinner"<Spinner />Examples
Input Group
A spinner placed in an InputGroupAddon to indicate that the input is performing a real-time lookup or validation.
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import { Spinner } from "@/components/ui/spinner";
export default function Particle() {
return (
<InputGroup>
<InputGroupInput disabled placeholder="Processing…" type="search" />
<InputGroupAddon>
<Spinner />
</InputGroupAddon>
</InputGroup>
);
}
In Buttons
Replaces the button label with a spinner and "Loading…" text while an async action is in progress, then restores the original label.
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
export function Pattern() {
return (
<div className="flex flex-wrap items-center justify-center gap-4">
<Button>
<Spinner data-icon="inline-start" /> Processing…
</Button>
<Button disabled variant="outline">
<Spinner data-icon="inline-start" /> Loading…
</Button>
<Button aria-label="Loading" disabled size="icon" variant="outline">
<Spinner />
</Button>
</div>
);
}
In Empty State
Displays a full centered spinner inside an Empty container — used as the initial loading state before content arrives.
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Spinner } from "@/components/ui/spinner";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<Empty className="min-h-75 w-full max-w-md">
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner className="size-4" />
</EmptyMedia>
<EmptyTitle>Loading projects</EmptyTitle>
<EmptyDescription>
Please wait while we fetch your project data. This should only take
a moment.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button disabled variant="outline">
Cancel
</Button>
</EmptyContent>
</Empty>
</div>
);
}
Overlay
A semi-transparent overlay covers the content area and centers a spinner while a background operation completes.
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
export function Pattern() {
return (
<div className="w-full max-w-xs overflow-hidden">
{/* Card */}
<Card>
<CardHeader>
<CardTitle>Monthly Report</CardTitle>
<CardDescription>Revenue and growth metrics.</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Revenue</span>
<span className="font-medium">$12,450</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Growth</span>
<span className="font-medium">+18.2%</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Users</span>
<span className="font-medium">1,248</span>
</div>
</CardContent>
</Card>
{/* Overlay */}
<Card className="absolute inset-0 z-10 bg-background/80 backdrop-blur-xs">
<CardContent className="flex grow flex-col items-center justify-center gap-2">
<Spinner className="size-5 opacity-60" />
<span className="text-muted-foreground text-sm">
Refreshing data...
</span>
</CardContent>
</Card>
</div>
);
}
Full Page
A full-viewport centered spinner for top-level route or app initialization loading — shown before the page content renders.
Setting up your workspace
This may take a few seconds...
import { Card, CardContent } from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
export function Pattern() {
return (
<Card className="min-h-50 w-full max-w-xs">
<CardContent className="flex grow flex-col items-center justify-center gap-4">
<Spinner className="size-4 opacity-50" />
<div className="flex flex-col items-center gap-1">
<p className="font-medium text-sm">Setting up your workspace</p>
<p className="text-muted-foreground text-xs">
This may take a few seconds...
</p>
</div>
</CardContent>
</Card>
);
}
Inline Text
The spinner sits inline next to "Loading…" text for compact in-content loading indicators, such as inside a table row or list item.
import { Spinner } from "@/components/ui/spinner";
export function Pattern() {
return (
<div className="mx-auto flex w-full max-w-xs flex-col gap-3">
<div className="flex items-center gap-2">
<Spinner className="size-3.5" />
<span className="text-muted-foreground text-sm">
Checking availability...
</span>
</div>
<div className="flex items-center gap-2">
<Spinner className="size-3.5 text-success" />
<span className="text-sm">
<span className="font-medium text-success">Connected</span>
<span className="text-muted-foreground"> — syncing data</span>
</span>
</div>
<div className="flex items-center gap-2">
<Spinner className="size-3.5 text-warning" />
<span className="text-sm">
<span className="font-medium text-warning">Reconnecting</span>
<span className="text-muted-foreground"> — attempt 3 of 5</span>
</span>
</div>
</div>
);
}
Card Overlay
A spinner overlay scoped to a single card rather than the full page, blocking interaction only for that card's content area.
Dashboard Overview
Monthly revenue and user statistics for the current period.
Revenue
$12,450
Users
1,234
import { Card, CardContent } from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
export function Pattern() {
return (
<Card className="relative w-full max-w-xs">
<CardContent className="space-y-3 p-4">
<h3 className="font-semibold text-sm">Dashboard Overview</h3>
<p className="text-muted-foreground text-sm">
Monthly revenue and user statistics for the current period.
</p>
<div className="grid grid-cols-2 gap-4">
<div className="rounded-md border p-3">
<p className="text-muted-foreground text-xs">Revenue</p>
<p className="font-bold text-lg">$12,450</p>
</div>
<div className="rounded-md border p-3">
<p className="text-muted-foreground text-xs">Users</p>
<p className="font-bold text-lg">1,234</p>
</div>
</div>
</CardContent>
{/* Overlay */}
<Card className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-xs">
<CardContent className="flex grow flex-col items-center justify-center gap-2">
<Spinner className="size-4 opacity-60" />
</CardContent>
</Card>
</Card>
);
}
Color variants
Demonstrates the available spinner color options — default, muted, primary, destructive, and custom foreground colors.
import { Spinner } from "@/components/ui/spinner";
export function Pattern() {
return (
<div className="flex items-center justify-center gap-4">
<Spinner className="size-4 text-blue-500" />
<Spinner className="size-4 text-green-500" />
<Spinner className="size-4 text-red-500" />
<Spinner className="size-4 text-yellow-500" />
<Spinner className="size-4 text-purple-500" />
</div>
);
}
Page Transition
A backdrop overlay with a centered spinner simulates a client-side route navigation. The overlay fades in on click and clears after the transition completes.
Dashboard
Click to simulate navigation
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
export default function Particle() {
const [loading, setLoading] = useState(false);
function handleNavigate() {
setLoading(true);
setTimeout(() => setLoading(false), 2000);
}
return (
<div className="relative flex h-48 w-full max-w-sm items-center justify-center overflow-hidden rounded-xl border bg-background">
{loading && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-background/80 backdrop-blur-sm">
<Spinner className="size-8 text-primary" />
<p className="text-muted-foreground text-sm">Loading page…</p>
</div>
)}
<div className="space-y-2 text-center">
<p className="font-medium">Dashboard</p>
<p className="text-muted-foreground text-sm">
Click to simulate navigation
</p>
<Button onClick={handleNavigate} size="sm" variant="outline">
Navigate
</Button>
</div>
</div>
);
}
File Upload
An upload card that shows a spinner in place of the icon while the file transfers. The button is disabled during upload and updates its label to reflect the current state.
Ready to upload
Max file size 25 MB
"use client";
import { UploadCloudIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
type Status = "idle" | "uploading" | "done";
export default function Particle() {
const [status, setStatus] = useState<Status>("idle");
function handleUpload() {
setStatus("uploading");
setTimeout(() => setStatus("done"), 2500);
}
return (
<div className="w-full max-w-sm space-y-4 rounded-xl border p-5">
<div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
{status === "uploading" ? (
<Spinner className="size-5 text-primary" />
) : (
<UploadCloudIcon
aria-hidden="true"
className={`size-5 ${status === "done" ? "text-green-500" : "text-muted-foreground"}`}
/>
)}
</div>
<div className="flex-1">
<p className="font-medium text-sm">
{status === "idle" && "Ready to upload"}
{status === "uploading" && "Uploading report.pdf…"}
{status === "done" && "Upload complete"}
</p>
<p className="text-muted-foreground text-xs">
{status === "idle" && "Max file size 25 MB"}
{status === "uploading" && "Please wait"}
{status === "done" && "report.pdf · 4.2 MB"}
</p>
</div>
</div>
<Button
className="w-full"
disabled={status === "uploading" || status === "done"}
onClick={handleUpload}
variant={status === "done" ? "outline" : "default"}
>
{status === "uploading" && (
<Spinner
aria-hidden="true"
className="size-4"
data-icon="inline-start"
/>
)}
{status === "idle" && "Upload File"}
{status === "uploading" && "Uploading…"}
{status === "done" && "Uploaded"}
</Button>
</div>
);
}
Table Loading
A data table where the rows are replaced with spinner placeholders while a refresh is in progress. The toolbar button also swaps its icon for a spinner during the fetch.
Invoices
Acme Corp
INV-001
$1,200
Paid
Globex Inc
INV-002
$840
Pending
Initech
INV-003
$3,500
Paid
"use client";
import { RefreshCwIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
const rows = [
{ amount: "$1,200", customer: "Acme Corp", id: "INV-001", status: "Paid" },
{ amount: "$840", customer: "Globex Inc", id: "INV-002", status: "Pending" },
{ amount: "$3,500", customer: "Initech", id: "INV-003", status: "Paid" },
];
export default function Particle() {
const [loading, setLoading] = useState(false);
function handleRefresh() {
setLoading(true);
setTimeout(() => setLoading(false), 1800);
}
return (
<div className="w-full max-w-md overflow-hidden rounded-xl border">
<div className="flex items-center justify-between border-b px-4 py-3">
<p className="font-medium text-sm">Invoices</p>
<Button onClick={handleRefresh} size="sm" variant="ghost">
{loading ? (
<Spinner aria-hidden="true" className="size-4" />
) : (
<RefreshCwIcon aria-hidden="true" className="size-4" />
)}
Refresh
</Button>
</div>
<div className="divide-y">
{loading
? Array.from({ length: 3 }).map((_, i) => (
<div
className="flex items-center justify-between px-4 py-3"
key={String(i)}
>
<div className="flex items-center gap-3">
<Spinner className="size-4 text-muted-foreground" />
<span className="text-muted-foreground text-sm">
Loading…
</span>
</div>
</div>
))
: rows.map((row) => (
<div
className="flex items-center justify-between px-4 py-3"
key={row.id}
>
<div>
<p className="font-medium text-sm">{row.customer}</p>
<p className="text-muted-foreground text-xs">{row.id}</p>
</div>
<div className="text-right">
<p className="font-semibold text-sm">{row.amount}</p>
<p
className={`text-xs ${row.status === "Paid" ? "text-green-600" : "text-muted-foreground"}`}
>
{row.status}
</p>
</div>
</div>
))}
</div>
</div>
);
}
Auth Form
A sign-in form whose submit button shows a spinner and "Signing in…" label while the request is in flight, then confirms success once it resolves.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
export default function Particle() {
const [loading, setLoading] = useState(false);
const [done, setDone] = useState(false);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setTimeout(() => {
setLoading(false);
setDone(true);
}, 2000);
}
return (
<form
className="w-full max-w-sm space-y-4 rounded-xl border p-5"
onSubmit={handleSubmit}
>
<p className="font-semibold">Sign in</p>
<Field>
<FieldLabel>Email</FieldLabel>
<Input
defaultValue="user@example.com"
disabled={loading || done}
type="email"
/>
</Field>
<Field>
<FieldLabel>Password</FieldLabel>
<Input
defaultValue="••••••••"
disabled={loading || done}
type="password"
/>
</Field>
<Button className="w-full" disabled={loading || done} type="submit">
{loading && (
<Spinner
aria-hidden="true"
className="size-4"
data-icon="inline-start"
/>
)}
{done ? "Signed in!" : loading ? "Signing in…" : "Sign in"}
</Button>
</form>
);
}
Sizes
All available spinner sizes — xs through 2xl — displayed side by side with labels for quick visual reference.
import { Spinner } from "@/components/ui/spinner";
const sizes = [
{ className: "size-3", label: "xs" },
{ className: "size-4", label: "sm" },
{ className: "size-5", label: "md" },
{ className: "size-6", label: "lg" },
{ className: "size-8", label: "xl" },
{ className: "size-10", label: "2xl" },
];
export default function Particle() {
return (
<div className="flex flex-wrap items-end justify-center gap-6">
{sizes.map(({ label, className }) => (
<div className="flex flex-col items-center gap-2" key={label}>
<Spinner className={`${className} text-primary`} />
<span className="text-muted-foreground text-xs">{label}</span>
</div>
))}
</div>
);
}

