# Loader

Eighteen loading animations with accessible labels, adjustable speed, and reduced motion support.

Page: https://sui.draco.dev/docs/components/loader

### Example: loader-demo

```tsx
import { Loader, loaderVariantNames } from "@workspace/ui/components/loader";
import type { ExampleProps } from "../types";
export default function Example({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  return (
    <div className="grid w-full grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-6">
      {loaderVariantNames.map((variant) => (
        <div
          key={variant}
          className="flex min-h-24 flex-col items-center justify-center gap-4 rounded-xl border bg-background p-3"
        >
          <Loader
            variant={variant}
            size={32}
            label={zh ? "正在加载" : "Loading"}
          />
          <span className="text-center font-mono text-muted-foreground text-sm">
            {variant}
          </span>
        </div>
      ))}
    </div>
  );
}
```

## Installation

```bash
bunx --bun shadcn@latest add @sui/loader
```

Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases.

```tsx
import { Loader } from "@workspace/ui/components/loader";
```

## Usage

```tsx
<Loader variant="dots" label="Loading messages" />
```

`Loader` provides eighteen distinct loading animations, all using the current text color. The root exposes `role="status"` and a loading label while decorative visuals remain hidden from assistive technology. Reduced motion changes stop CSS animations and character timers immediately, leaving a static indicator. Timers and media listeners are cleaned up on unmount. Server rendering uses static character frames to avoid hydration differences.

## Variants

| Variant | Animation |
| --- | --- |
| `spinner` | Rotating arc over a circular track |
| `dash-ring` | Stretching arc that travels smoothly around a rotating ring |
| `dots` | Three staggered bouncing dots |
| `bars` | Four staggered scaling bars |
| `dot-matrix` | A rippling 3 × 3 dot matrix |
| `dither` | A patterned 4 × 4 blinking square matrix |
| `ascii` | Rotating character frames |
| `ascii-line` | Rotating line character frames |
| `ascii-braille` | Braille ring character frames |
| `ascii-blocks` | Rising and falling block characters |
| `ascii-bounce` | Bouncing dot character frames |
| `morph` | Rotating circle, square, triangle, and hexagon morphs |
| `comet` | Rotating comet with a fading trail |
| `scramble` | Scrambled characters resolving to LOADING |
| `metaballs` | Two merging circular blobs |
| `newton` | Alternating end swings of a Newton cradle |
| `helix` | Opposing strands of oscillating dots |
| `percent` | Decorative looping percentage and short bar |

`percent` is a looping loading motif, not measured task progress. Use [Progress](/docs/components/progress) for actual progress.

## Sizes and colors

Use `sm` (16px), `default` (20px), `lg` (28px), or a numeric pixel size. Numeric sizes are clamped to 8–4096px; non-finite values use 20px. Set `className="text-primary"` to use the active theme color.

### Example: loader-size

```tsx
import { Loader } from "@workspace/ui/components/loader";
import type { ExampleProps } from "../types";
export default function Example({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  return (
    <div className="grid gap-6 text-primary">
      {(["spinner", "dash-ring"] as const).map((variant) => (
        <div key={variant} className="grid gap-3">
          <span className="font-mono text-muted-foreground text-sm">
            {variant}
          </span>
          <div className="flex items-center gap-6">
            {(["sm", "default", "lg", 40] as const).map((size) => (
              <div key={size} className="grid justify-items-center gap-3">
                <Loader
                  variant={variant}
                  size={size}
                  label={zh ? "正在加载" : "Loading"}
                />
                <span className="font-mono text-muted-foreground text-sm">
                  {typeof size === "number" ? `${size}px` : size}
                </span>
              </div>
            ))}
          </div>
        </div>
      ))}
      <div className="flex items-center justify-center gap-10">
        {[0.6, 2].map((speed) => (
          <div key={speed} className="grid justify-items-center gap-3">
            <Loader
              variant="dots"
              size={28}
              speed={speed}
              label={zh ? "正在加载" : "Loading"}
            />
            <span className="text-muted-foreground text-sm">{speed}s</span>
          </div>
        ))}
      </div>
    </div>
  );
}
```

## Animation speed

`speed` controls the base cycle in seconds and defaults to `1`. Smaller values are faster; values are clamped to `0.1`–`86400` and non-finite values fall back to `1`. Character timers have a minimum interval of 10 milliseconds. Complex animations use fixed multiples of the base cycle.

```tsx
<Loader variant="dots" speed={0.6} label="Loading" />
<Loader variant="metaballs" speed={2} label="Loading" />
```

## Button loading state

Render the loader only while work is in progress and disable repeated submissions. This example simulates a local save and cleans up its timer on unmount.

### Example: loader-button

```tsx
import { Button } from "@workspace/ui/components/button";
import { Loader } from "@workspace/ui/components/loader";
import { useEffect, useRef, useState } from "react";
import type { ExampleProps } from "../types";
export default function Example({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const stateLabels = zh
    ? {
        saving: "正在保存…",
        save: "保存设置",
        saved: "预览设置已保存",
      }
    : {
        saving: "Saving…",
        save: "Save settings",
        saved: "Preview settings saved.",
      };
  const [loading, setLoading] = useState(false);
  const [saved, setSaved] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  useEffect(
    () => () => {
      if (timer.current !== undefined) clearTimeout(timer.current);
    },
    [],
  );
  return (
    <div className="grid justify-items-center gap-3">
      <Button
        disabled={loading}
        onClick={() => {
          setLoading(true);
          setSaved(false);
          timer.current = setTimeout(() => {
            setLoading(false);
            setSaved(true);
          }, 1200);
        }}
      >
        {loading ? (
          <Loader
            size="sm"
            variant="spinner"
            label={zh ? "正在保存" : "Saving"}
          />
        ) : null}
        {loading ? stateLabels.saving : stateLabels.save}
      </Button>
      <span role="status" className="min-h-5 text-muted-foreground text-sm">
        {saved ? stateLabels.saved : ""}
      </span>
    </div>
  );
}
```

## API

| Prop | Type | Default |
| --- | --- | --- |
| `variant` | `LoaderVariant`, see all eighteen variants above | `"spinner"` |
| `size` | `"sm" \| "default" \| "lg" \| number` | `"default"` |
| `speed` | `number`, base cycle in seconds | `1` |
| `label` | `string` | `"Loading"` |
| `render` | React element or render function | `span` |

Standard `span` props, refs, styles, and `className` are supported. The root exposes `data-slot="loader"` and `data-variant`. Labels describe actual activity; the percentage motif is decorative and does not expose progress values to assistive technology. See [Base UI composition](https://base-ui.com/react/handbook/composition) for `render` and ref behavior.
