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.json
Loader · CSSno JS
one file · three dials

RingSpinneresc or double-click to exit full screendouble-tap to exit

loaders/ring-spinner.tsx
"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