Skip to content
goodtabs UI
BrowseBrowse components

Spinner set

Ring, dots and bars spinners that hold still under reduced motion.

npx shadcn@latest add https://ui.goodtabs.co/r/spinner-set.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/spinner-set

LoadingLoadingLoading

Three shapes at their default size.

Beside a label

Fetching registryFetching the registry

Inside a disabled button and next to a line of text.

Usage

import { RingSpinner } from "@/components/ui/spinner-set"

<RingSpinner label="Saving changes" />

Props

PropTypeDefaultDescription
labelstring"Loading"Text for assistive tech, hidden visually. Say what is loading.
classNamestringApplies to the status wrapper.

Source

spinner-set/spinner-set.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type SpinnerProps = Omit<React.ComponentProps<"span">, "children"> & {
  /** Read by screen readers. Defaults to "Loading". */
  label?: string;
};

/** Shared shell: a status region with a hidden text label. */
function SpinnerShell({
  label = "Loading",
  className,
  children,
  ...props
}: SpinnerProps & { children: React.ReactNode }) {
  return (
    <span role="status" className={cn("inline-flex items-center", className)} {...props}>
      {children}
      <span className="sr-only">{label}</span>
    </span>
  );
}

/** A ring with a quarter arc. Under reduced motion it holds still. */
function RingSpinner({ className, ...props }: SpinnerProps) {
  return (
    <SpinnerShell className={className} {...props}>
      <span
        aria-hidden="true"
        className="size-4 rounded-full border-2 border-muted-foreground/30 border-t-foreground motion-safe:animate-spin"
      />
    </SpinnerShell>
  );
}

/** Three dots that pulse in turn. Under reduced motion all three show at full strength. */
function DotsSpinner({ className, ...props }: SpinnerProps) {
  return (
    <SpinnerShell className={cn("gap-1", className)} {...props}>
      {[0, 1, 2].map((i) => (
        <span
          key={i}
          aria-hidden="true"
          className="size-1.5 rounded-full bg-foreground motion-safe:animate-pulse"
          style={{ animationDelay: `${i * 160}ms` }}
        />
      ))}
    </SpinnerShell>
  );
}

/** Four bars that fade in turn, a quiet choice for dense toolbars. */
function BarsSpinner({ className, ...props }: SpinnerProps) {
  return (
    <SpinnerShell className={cn("h-4 items-end gap-0.5", className)} {...props}>
      {[0, 1, 2, 3].map((i) => (
        <span
          key={i}
          aria-hidden="true"
          className="w-0.5 rounded-full bg-foreground motion-safe:animate-pulse"
          style={{ height: `${50 + i * 16}%`, animationDelay: `${i * 140}ms` }}
        />
      ))}
    </SpinnerShell>
  );
}

export { RingSpinner, DotsSpinner, BarsSpinner };

For AI agents

Hand your agent this page, the markdown at /components/spinner-set/prompt.md, or the Copy AI prompt button above.

When to use

  • A short wait inside a button or beside a line of status text
  • Work of unknown length where no percentage is available

When not to use

  • Content areas with a known shape: use skeletons
  • Anything with a measurable percentage: use a progress bar

Agent notes

  • Each spinner is role=status with an sr-only label, so it is announced once on mount
  • Animation is motion-safe only: under reduced motion the ring shows its still arc and the dots and bars show at full strength
  • Set the label to the specific task, not just Loading
  • Server component, no JavaScript