@cf/spinner
Spinner
A spinning Phosphor circle-notch that announces "Loading" for anything in progress.
Saving
CircleNotchIcon from Phosphor with animate-spin, size-4, role="status" and an aria-label of Loading. Because it takes the icon's props, size, weight and className all work.
In buttons it takes the place of the label while a request is in flight (the access form, the owner login). In the note editors it sits absolutely positioned where the save state shows, so text beside it doesn't jump.
import { Spinner } from "@/components/ui/spinner";
<Button disabled={pending} type="submit">
{pending ? <Spinner data-icon="inline-start" /> : "Sign in"}
</Button>Rules
The same rules ship in DESIGN.md and the MCP server, for the agents building with this.
- should
Inside a
Button, give the Spinnerdata-icon="inline-start"so the button sizes it like any other icon.data-icon-inside-button
- should
Don't add a second visually hidden Loading label next to it; the Spinner already has
role="status"andaria-label="Loading". Overridearia-labelwhen something more specific is true, like Saving.no-duplicate-loading-text
- should
When a Spinner replaces a button's label, also set
disabledon the button so it can't be submitted twice.disable-while-pending
- may
Position a status Spinner with
className="absolute"over a reserved space when it appears and disappears next to text, as the notes save indicator does.avoid-layout-shift
Install
bunx shadcn@latest add @cf/spinnerNeeds the @cf registry in your components.json once. The theme and any sibling components come along automatically.
Source
spinner.tsxShowHide
"use client";
import { CircleNotchIcon } from "@phosphor-icons/react";
import { cn } from "cn";
function Spinner({
className,
...props
}: React.ComponentProps<typeof CircleNotchIcon>) {
return (
<CircleNotchIcon
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
data-slot="spinner"
role="status"
{...props}
/>
);
}
export { Spinner };