Spinner
A turning ring for work in flight.
import { Div } from "@implementjs/core";
import { Button } from "@/lib/components/ui/button";
import { Spinner } from "@/lib/components/ui/spinner";
export default function SpinnerDemo() {
return Div(
{ class: "flex flex-wrap items-center justify-center gap-6" },
Spinner({ "aria-label": "Loading projects" }),
Spinner({ class: "size-6 text-muted-foreground", "aria-label": "Loading" }),
Button({ disabled: true }, Spinner(), "Saving"),
);
}Installation
npx jsrepo add @implementjs/ui/spinner
It installs @implementjs/lucide at the same time.
Copy the file below to src/lib/components/ui/spinner.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too. Then, on top of @implementjs/core and @implementjs/primitives:
npm install @implementjs/lucide
import type { SvgProps } from "@implementjs/core";
import { LoaderCircleIcon } from "@implementjs/lucide";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type SpinnerProps = SvgProps;
/**
* A turning ring for work in flight. It carries `role="status"` and a label,
* so a screen reader announces the wait — pass `aria-label` to say what for.
*/
export const Spinner = createComponent(function Spinner({
class: className,
...props
}: SpinnerProps) {
return LoaderCircleIcon({
role: "status",
"aria-label": "Loading",
...props,
"data-slot": "spinner",
class: cn("size-4 animate-spin motion-reduce:animate-none", className),
});
});Usage
import { Spinner } from "@/lib/components/ui/spinner";
Spinner({ "aria-label": "Loading projects" });
It carries role="status" and a default label, so a screen reader announces the wait. Pass aria-label to say what is being waited on.
In a button
A button that is working should say so and stop accepting clicks:
Button({ disabled: true }, Spinner(), "Saving");
The button's base styles already size and space an icon inside it, so the spinner needs no classes of its own here.
Determinate work
A spinner says something is happening. When you know how far along it is, say that instead — use progress.
Reduced motion
The spin stops under prefers-reduced-motion, leaving a static ring.
API Reference
Spinner
A turning ring for work in flight. Carries role="status" and a default label; pass aria-label to say what is loading. Size and color it with classes, like any icon. Renders a LoaderCircleIcon; extra props are forwarded onto it.