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.
PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Icon size of the spinner.
labelstring"Loading"Screen-reader status text announced while loading.
classNamestring—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 };
}