Spinner
Eight ticks whose brightness walks round the circle while nothing rotates.
Indexing 4 sources
The idea
An arc spinning at 14px smears into a grey ring; eight ticks holding still while their brightness walks round stay legible at any size and cost only opacity. It takes the colour of the text around it, so it is light on an ink button and dark on the page. Under reduced motion the walk stops and the ticks hold as a still gradient, which still reads as working.
Install
npm i mihirmodi-ui motionThe tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.
Usage
import { Spinner } from "mihirmodi-ui";
// Takes the colour of the text around it.
<Spinner size={16} label="Loading" />Props
| Prop | Type | Notes |
|---|---|---|
| sizenumber | number= 16 | Diameter in px. |
| labelstring | string | Makes it a status with this name. Omit it inside something already marked busy. |
Source
Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.
"use client";
export type SpinnerProps = {
/** Diameter in px. */
size?: number;
/** The accessible name. Omit it when the spinner sits inside something that already says it is busy. */
label?: string;
className?: string;
};
const TICKS = 8;
/**
* Eight ticks whose brightness walks round the circle, rather than an arc that
* spins. A spinning arc is one shape moving fast, and at 14px it smears into a
* grey ring; ticks that fade in turn are eight shapes holding still, so the
* motion stays legible at any size and costs only opacity.
*
* It takes the colour of the text around it, so on an ink button it is
* light and on the page it is dark — nothing to configure. Under reduced motion
* the walk stops and the ticks hold at a gradient, which still reads as
* "working" without anything moving.
*/
export function Spinner({ size = 16, label, className = "" }: SpinnerProps) {
return (
<span
role={label ? "status" : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
className={`ui-spinner relative inline-block shrink-0 ${className}`}
style={{ width: size, height: size }}
>
{Array.from({ length: TICKS }, (_, i) => (
<span
key={i}
className="absolute inset-0"
style={{ transform: `rotate(${(360 / TICKS) * i}deg)` }}
>
<span
className="ui-spinner-tick absolute left-1/2 top-0 h-[28%] w-[11%] min-w-[1.5px] -translate-x-1/2 rounded-full bg-current"
style={{ animationDelay: `${(-1 + i / TICKS).toFixed(3)}s`, opacity: 0.25 + (0.75 * i) / TICKS }}
/>
</span>
))}
</span>
);
}