Spinner set
Ring, dots and bars spinners that hold still under reduced motion.
npx shadcn@latest add https://ui.goodtabs.co/r/spinner-set.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/spinner-set
LoadingLoadingLoading
Three shapes at their default size.
Beside a label
Fetching registryFetching the registry
Inside a disabled button and next to a line of text.
Usage
import { RingSpinner } from "@/components/ui/spinner-set"
<RingSpinner label="Saving changes" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | "Loading" | Text for assistive tech, hidden visually. Say what is loading. |
| className | string | Applies to the status wrapper. |
Source
spinner-set/spinner-set.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
type SpinnerProps = Omit<React.ComponentProps<"span">, "children"> & {
/** Read by screen readers. Defaults to "Loading". */
label?: string;
};
/** Shared shell: a status region with a hidden text label. */
function SpinnerShell({
label = "Loading",
className,
children,
...props
}: SpinnerProps & { children: React.ReactNode }) {
return (
<span role="status" className={cn("inline-flex items-center", className)} {...props}>
{children}
<span className="sr-only">{label}</span>
</span>
);
}
/** A ring with a quarter arc. Under reduced motion it holds still. */
function RingSpinner({ className, ...props }: SpinnerProps) {
return (
<SpinnerShell className={className} {...props}>
<span
aria-hidden="true"
className="size-4 rounded-full border-2 border-muted-foreground/30 border-t-foreground motion-safe:animate-spin"
/>
</SpinnerShell>
);
}
/** Three dots that pulse in turn. Under reduced motion all three show at full strength. */
function DotsSpinner({ className, ...props }: SpinnerProps) {
return (
<SpinnerShell className={cn("gap-1", className)} {...props}>
{[0, 1, 2].map((i) => (
<span
key={i}
aria-hidden="true"
className="size-1.5 rounded-full bg-foreground motion-safe:animate-pulse"
style={{ animationDelay: `${i * 160}ms` }}
/>
))}
</SpinnerShell>
);
}
/** Four bars that fade in turn, a quiet choice for dense toolbars. */
function BarsSpinner({ className, ...props }: SpinnerProps) {
return (
<SpinnerShell className={cn("h-4 items-end gap-0.5", className)} {...props}>
{[0, 1, 2, 3].map((i) => (
<span
key={i}
aria-hidden="true"
className="w-0.5 rounded-full bg-foreground motion-safe:animate-pulse"
style={{ height: `${50 + i * 16}%`, animationDelay: `${i * 140}ms` }}
/>
))}
</SpinnerShell>
);
}
export { RingSpinner, DotsSpinner, BarsSpinner };
For AI agents
Hand your agent this page, the markdown at /components/spinner-set/prompt.md, or the Copy AI prompt button above.
When to use
- A short wait inside a button or beside a line of status text
- Work of unknown length where no percentage is available
When not to use
- Content areas with a known shape: use skeletons
- Anything with a measurable percentage: use a progress bar
Agent notes
- Each spinner is role=status with an sr-only label, so it is announced once on mount
- Animation is motion-safe only: under reduced motion the ring shows its still arc and the dots and bars show at full strength
- Set the label to the specific task, not just Loading
- Server component, no JavaScript