RingSpinner
The one everyone reaches for, drawn once from the seam: a conic tail masked to a ring and rotated on the compositor, so it costs nothing to spin.
Install
$ npx shadcn@latest add wisp.pouriah.com/r/ring-spinner.jsonLoader · CSSno JS
one file · three dials
"use client";
import * as React from "react";
import { cx } from "@pouriahlabs/wisp-ui";
/**
* RingSpinner — the spinner everyone reaches for, drawn once from the seam.
*
* A conic gradient that fades from transparent up to your two tokens forms the
* tail; a radial mask cuts the disc down to a ring. The element itself is what
* rotates, so the spin runs as a compositor transform rather than repainting a
* gradient every frame — it costs nothing to leave on a page.
*
* No JavaScript drives it: the animation is entirely in the stylesheet, and
* under reduced motion it holds a static ring rather than a blank box.
*/
export interface RingSpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
/** Token for the tail's leading edge. */
fromToken?: string;
/** Token for the tail's head, where the ring is brightest. */
toToken?: string;
/** Outer diameter, as any CSS length. */
size?: string;
/** Ring thickness, as any CSS length. */
width?: string;
/** Seconds per revolution. */
duration?: number;
/** Accessible status text announced while the spinner is on screen. */
label?: string;
}
export function RingSpinner({
fromToken = "--wisp",
toToken = "--lantern",
size = "2.5rem",
width = "3px",
duration = 0.9,
label = "Loading",
className,
style,
...props
}: RingSpinnerProps) {
return (
<div
className={cx("wisp-spinner", className)}
role="status"
aria-label={label}
style={
{
"--wisp-spinner-from": `var(${fromToken})`,
"--wisp-spinner-to": `var(${toToken})`,
"--wisp-spinner-size": size,
"--wisp-spinner-width": width,
"--wisp-spinner-duration": `${duration}s`,
...style,
} as React.CSSProperties
}
{...props}
/>
);
}
Certified against the contract
- ✓Reduced motion paints one composed still frame — never a blank box
- ✓Pauses off-screen and in hidden tabs
- ✓Re-reads design tokens when the theme changes
- ✓SSR-safe: no hydration mismatch, no layout shift
- ✓Decorative layers are aria-hidden and pointer-events-none
- ✓Device pixel ratio clamped
- ✓Zero runtime dependencies beyond React