Spinner
An indeterminate loading indicator for buttons, panels and inline content.
Loading
Installation
Terminal
pnpm dlx @dowel-ui/cli add spinnernpm packages installed: class-variance-authority.
Accessibility
Renders aria-hidden by default so it does not announce inside controls that already expose a busy state. Pass `label` to announce it standalone via role="status".
Props
Spinner
| Prop | Type | Default |
|---|---|---|
labelAnnounced to assistive technology while the spinner is visible. Omit it when the spinner sits inside a control that already communicates its busy state (a loading Button, for example) — announcing twice is worse than not announcing at all. | string | — |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" |
Plus every attribute of <svg> except children.
Quality
8/8 checks, measured from the source and its tests
- Tested — passes
- axe assertion — passes
- Keyboard tested — does not apply
- Storybook examples — passes
- Accessibility documented — passes
- Semantic tokens only — passes
- Motion from tokens — passes
- className merged — passes
- Visible focus — does not apply
- No fixed widths — passes
Source
This is exactly what dowel add spinner writes into your project, with imports rewritten to your own path alias.
ui/spinner.tsx
import { cva, type VariantProps } from "class-variance-authority";
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils";
const spinnerVariants = cva("animate-spin text-current", {
variants: {
size: {
xs: "size-3",
sm: "size-3.5",
md: "size-4",
lg: "size-5",
xl: "size-6",
},
},
defaultVariants: {
size: "md",
},
});
export interface SpinnerProps
extends Omit<ComponentPropsWithRef<"svg">, "children">, VariantProps<typeof spinnerVariants> {
/**
* Announced to assistive technology while the spinner is visible.
*
* Omit it when the spinner sits inside a control that already communicates
* its busy state (a loading Button, for example) — announcing twice is worse
* than not announcing at all.
*/
label?: string;
}
/** Indeterminate loading indicator. */
export function Spinner({ className, size, label, ...props }: SpinnerProps) {
return (
<>
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
// Exempt from the reduced-motion blanket: this is not decoration. A
// spinner that stops turning reports that the application has hung,
// which is a worse experience than the motion it was avoiding. It is
// slowed instead — see --motion-scale-indicator.
data-motion="indicator"
className={cn(spinnerVariants({ size }), className)}
{...props}
>
<circle
cx="12"
cy="12"
r="9.5"
stroke="currentColor"
strokeWidth="2.5"
opacity="0.22"
/>
<path
d="M12 2.5a9.5 9.5 0 0 1 9.5 9.5"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
/>
</svg>
{label ? (
<span role="status" className="sr-only">
{label}
</span>
) : null}
</>
);
}
export { spinnerVariants };