Spinner
Indeterminate loading indicator with an accessible status label.
import { Spinner }from "@/components/ui"
Specification
Indicates an indeterminate loading state with a spinning icon and an accessible status label.
Props (3)
Every prop this component accepts, its type, default, and how to use it.
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | "md" | Icon size of the spinner. |
| label | string | "Loading" | Screen-reader status text announced while loading. |
| className | string | — | Extra classes merged onto the wrapper. |
Sizes
LoadingLoadingLoading
Full source
The complete component, read from
src/components/ui/spinner.tsx.spinner.tsx
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
type SpinnerSize = "sm" | "md" | "lg";
const SIZE: Record<SpinnerSize, number> = { sm: 14, md: 18, lg: 24 };
interface SpinnerProps {
size?: SpinnerSize;
label?: string;
className?: string;
}
export function Spinner({ size = "md", label = "Loading", className }: SpinnerProps) {
return (
<span role="status" className={cn("inline-flex items-center gap-2 text-text-secondary", className)}>
<Loader2 size={SIZE[size]} className="animate-spin text-accent" aria-hidden />
<span className="sr-only">{label}</span>
</span>
);
}Dependencies
To use Spinner you also need the following. Copy these into your project alongside it.
Recommended folder structure
project structure
src/components/ui/ └─ spinner.tsx src/lib/ └─ utils.ts
Dependency files
These supporting files aren’t components — copy each one into the matching path above.
src/lib/utils.ts
utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* Merge class names with clsx semantics + Tailwind conflict resolution.
* Prefer this in all components.
*/
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
/**
* Concatenate class names, filtering out falsy values.
* @deprecated Prefer {@link cn} — it also resolves Tailwind class conflicts.
*/
export function cx(...args: (string | false | null | undefined)[]): string {
return args.filter(Boolean).join(" ");
}
/**
* Focus ring style helper.
* @deprecated Focus rings are now handled globally via the
* `:focus-visible` rule in `globals.css` and the `focus-visible:*`
* utilities. This remains only for backwards compatibility.
*/
export function ring(color: string = "var(--color-accent)") {
return { outlineColor: color };
}