@cf/ui

@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 Spinner data-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" and aria-label="Loading". Override aria-label when something more specific is true, like Saving.

    no-duplicate-loading-text

  • should

    When a Spinner replaces a button's label, also set disabled on 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/spinner

Needs the @cf registry in your components.json once. The theme and any sibling components come along automatically.

Source

spinner.tsxShow
"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 };