work under way: a ring with a turning arc, still under reduced motion
npx shadcn add @ag/spinnerAlready installed? Preview an update
npx shadcn add @ag/spinner --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-10-10
- New: work under way, a ring with a turning arc in the text's colour (
sm,md,lg), still with reduced motion, a status a screen reader hears once by itslabel; on glass the arc catches the rim's light.
also called loader, loading
shelf controls
uses material
used by components, button, form, toast, deployment
the demo above, verbatim
"use client"
import { Sample } from "@/app/samples"
import { MaterialProvider } from "@/registry/base-nova/ui/material"
import { Spinner } from "@/registry/base-nova/ui/spinner"
// #region spinners
function Spinners() {
return (
<div className="flex items-center gap-4 text-sm">
<Spinner size="sm" />
<Spinner />
<Spinner size="lg" label="saving" />
<span className="flex items-center gap-2 text-current/70">
<Spinner size="sm" label="reading frames" /> reading frames
</span>
</div>
)
}
// #endregion
export default function SpinnerDemo() {
return (
<div className="space-y-8">
<Sample
name="materials"
title="three sizes, plain and glass"
with="spinners"
>
<div className="grid gap-4 sm:grid-cols-2">
<Spinners />
<MaterialProvider material="glass">
<div className="rounded-xl bg-[url(/lightbox/peaks.jpg)] bg-cover bg-center p-6 text-white">
<Spinners />
</div>
</MaterialProvider>
</div>
</Sample>
<Sample
name="sizes"
glance
title="three sizes, one with words"
with="spinners"
>
<Spinners />
</Sample>
</div>
)
}the source, every file an install writes
"use client"
import type * as React from "react"
import { cn } from "@/lib/utils"
import { type Material, useMaterial } from "@/registry/base-nova/ui/material"
// Work under way: a ring with a turning arc, in the text's colour, so it reads
// wherever it sits (a button, a row, a field). It turns only while it is shown, which
// is only while the work runs: nothing here spins on its own. With reduced motion
// the arc holds still. A screen reader hears its `label` once (role status).
// Glass: the arc catches the glass's rim light, a soft glow round it.
export type SpinnerSize = "sm" | "md" | "lg"
const SIDE: Record<SpinnerSize, string> = {
sm: "size-3.5",
md: "size-4",
lg: "size-5",
}
/** The ring's radius in a 16-unit box and its arc's length: a quarter turn. */
const R = 6.5
const ARC = (2 * Math.PI * R) / 4
export type SpinnerProps = Omit<React.ComponentProps<"span">, "children"> & {
size?: SpinnerSize
/** What is happening, for a screen reader ("saving"). */
label?: string
material?: Material
}
export function Spinner({
size = "md",
label = "loading",
material,
className,
...props
}: SpinnerProps) {
const m = useMaterial(material)
return (
<span
role="status"
aria-label={label}
data-slot="spinner"
data-material={m}
className={cn(
"inline-block shrink-0",
SIDE[size],
m === "glass" &&
"[filter:drop-shadow(0_0_2px_color-mix(in_oklab,var(--glass-rim-ink,white)_60%,transparent))]",
className,
)}
{...props}
>
<svg
viewBox="0 0 16 16"
aria-hidden
className="size-full animate-spin [animation-duration:800ms] motion-reduce:animate-none"
>
<circle
cx={8}
cy={8}
r={R}
fill="none"
stroke="currentColor"
strokeWidth={1.75}
opacity={m === "glass" ? 0.28 : 0.18}
/>
<circle
cx={8}
cy={8}
r={R}
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeDasharray={`${ARC} ${2 * Math.PI * R}`}
/>
</svg>
</span>
)
}