# Spinner

> Five CSS-only loaders (ring, dots, bars, pulse, orbit) with size prop and screen-reader label.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/spinner
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/spinner.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/spinner-demo.tsx"
import { Spinner, type SpinnerVariant } from "@/components/spinner"

const variants: SpinnerVariant[] = ["ring", "dots", "bars", "pulse", "orbit"]

export default function SpinnerDemo() {
  return (
    <div className="flex flex-col items-center gap-8">
      <div className="grid grid-cols-5 gap-6">
        {variants.map((variant) => (
          <div key={variant} className="flex flex-col items-center gap-3">
            <div className="flex size-20 items-center justify-center rounded-2xl border bg-card">
              <Spinner variant={variant} size={36} />
            </div>
            <span className="text-xs capitalize text-muted-foreground">{variant}</span>
          </div>
        ))}
      </div>
      <div className="flex items-end gap-6">
        <Spinner size={16} />
        <Spinner size={24} className="text-emerald-500" />
        <Spinner size={32} className="text-amber-500" variant="orbit" />
        <Spinner size={48} className="text-sky-500" variant="pulse" />
      </div>
      <div className="inline-flex items-center gap-2 rounded-lg border bg-card px-3 py-2 text-sm text-muted-foreground">
        <Spinner size={16} variant="dots" label="Saving" /> Saving changes
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/spinner.tsx"
import * as React from "react"

import { cn } from "@/lib/utils"

export type SpinnerVariant = "ring" | "dots" | "bars" | "pulse" | "orbit"

export interface SpinnerProps extends Omit<React.ComponentProps<"span">, "children"> {
  variant?: SpinnerVariant
  /** Box size in px */
  size?: number
  /** Screen-reader label */
  label?: string
}

const css = `
@keyframes atelier-sp-spin{to{transform:rotate(360deg)}}
@keyframes atelier-sp-dot{0%,80%,100%{transform:scale(.4);opacity:.4}40%{transform:scale(1);opacity:1}}
@keyframes atelier-sp-bar{0%,100%{transform:scaleY(.35)}50%{transform:scaleY(1)}}
@keyframes atelier-sp-pulse{0%{transform:scale(.2);opacity:.9}100%{transform:scale(1);opacity:0}}
.atelier-sp-spin{animation:atelier-sp-spin .8s linear infinite}
.atelier-sp-orbit{animation:atelier-sp-spin 1.2s linear infinite}
.atelier-sp-dot{animation:atelier-sp-dot 1.2s ease-in-out infinite}
.atelier-sp-bar{transform-origin:center;animation:atelier-sp-bar 1s ease-in-out infinite}
.atelier-sp-pulse{animation:atelier-sp-pulse 1.4s ease-out infinite}
@media (prefers-reduced-motion:reduce){
.atelier-sp-spin{animation-duration:3s}
.atelier-sp-orbit{animation-duration:4s}
.atelier-sp-dot{animation-duration:3.6s}
.atelier-sp-bar{animation-duration:3s}
.atelier-sp-pulse{animation-duration:4.2s}
}
`

export function Spinner({ variant = "ring", size = 24, label = "Loading", className, style, ...props }: SpinnerProps) {
  return (
    <span
      role="status"
      className={cn("relative inline-flex shrink-0 items-center justify-center text-primary", className)}
      style={{ width: size, height: size, ...style }}
      {...props}
    >
      <style href="atelier-spinner" precedence="default">
        {css}
      </style>
      <span className="sr-only">{label}</span>
      <span aria-hidden className="flex size-full items-center justify-center">
        {variant === "ring" && (
          <span
            className="atelier-sp-spin size-full rounded-full border-current border-t-transparent"
            style={{ borderWidth: Math.max(2, size / 10), borderStyle: "solid", opacity: 0.9 }}
          />
        )}
        {variant === "dots" && (
          <span className="flex size-full items-center justify-between">
            {[0, 1, 2].map((i) => (
              <span
                key={i}
                className="atelier-sp-dot rounded-full bg-current"
                style={{ width: size * 0.27, height: size * 0.27, animationDelay: `${i * 0.16}s` }}
              />
            ))}
          </span>
        )}
        {variant === "bars" && (
          <span className="flex size-full items-center justify-between">
            {[0, 1, 2, 3].map((i) => (
              <span
                key={i}
                className="atelier-sp-bar h-full rounded-full bg-current"
                style={{ width: size * 0.16, animationDelay: `${i * 0.12}s` }}
              />
            ))}
          </span>
        )}
        {variant === "pulse" && (
          <>
            <span className="atelier-sp-pulse absolute inset-0 rounded-full bg-current" />
            <span
              className="atelier-sp-pulse absolute inset-0 rounded-full bg-current"
              style={{ animationDelay: "0.7s" }}
            />
            <span className="rounded-full bg-current" style={{ width: size * 0.25, height: size * 0.25 }} />
          </>
        )}
        {variant === "orbit" && (
          <>
            <span className="rounded-full bg-current opacity-80" style={{ width: size * 0.22, height: size * 0.22 }} />
            <span className="atelier-sp-orbit absolute inset-0">
              <span
                className="absolute left-1/2 top-0 -translate-x-1/2 rounded-full bg-current"
                style={{ width: size * 0.2, height: size * 0.2 }}
              />
            </span>
            <span className="absolute inset-[6%] rounded-full border border-current opacity-20" />
          </>
        )}
      </span>
    </span>
  )
}
```
