# Loading

All 47 loading-ui animations in one component, selected with a typed variant prop.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```tsx
import { Loading } from "@/components/ui/loading";

export function Demo() {
  return <Loading variant="arc" size={32} />;
}
```

## Installation [#installation]

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/loading.json
```

The default installation target is components/ui/loading.tsx. Review the CLI's overwrite prompt before replacing local customizations.

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install cn motion
```

## Usage [#usage]

```tsx
import { Loading } from "@/components/ui/loading";

export function Demo() {
  return <Loading variant="arc" size={32} aria-label="Saving changes" />;
}
```

Omit `variant` to use `arc`. Installation adds the wrapper and variant list to `components/ui/`, plus all 47 loading-ui components and their MIT license to `components/loading-ui/`. Vandor UI distributes a source snapshot so the installed animations match the previews. Use your project's configured aliases if they differ.

The catalog matches loading-ui at the time of this release. Future upstream additions require a Vandor UI update; they do not silently become new variants.

## Variants [#variants]

Choose any name shown below with `variant`. This includes every visual component in loading-ui, not just circular spinners. The upstream `index`, `style`, and `utils` entries are infrastructure, not visual variants.

<ComponentPreview name="loading-variants" title="loading-variants.tsx">
  <LoadingVariants />
</ComponentPreview>

## Variant Configuration [#variant-configuration]

`variantProps` preserves each upstream component's options. TypeScript checks them against the selected `variant`; for example, `prompt` belongs to `terminal`, not `ring`.

```tsx
<Loading variant="dots-ring" variantProps={{ dots: 12, dotScale: 0.2 }} />
<Loading variant="bars" variantProps={{ bars: 5 }} />
<Loading variant="terminal" variantProps={{ prompt: "$" }} />
<Loading variant="square-grid" variantProps={{ size: 7, track: "·" }} />
<Loading
  variant="wandering-eyes"
  size={40}
  variantProps={{ eyeScale: 0.6, pupilScale: 0.25 }}
/>
```

`size` is the base visual size, in pixels or a CSS length. Text and glyph loaders use it as font size. Skeleton, eyes, and orbiting dots use proportional dimensions. `variantProps.size`, where supported, remains the upstream option (for example, the number of grid cells).

Use `className` for wrapper layout and inherited color. Use `variantProps.className` and `variantProps.style` for the visual itself:

```tsx
<Loading variant="ring" className="text-primary" size="2rem" />
<Loading
  variant="skeleton"
  aria-label="Loading profile"
  variantProps={{ className: "rounded-none", style: { width: 160, height: 24 } }}
/>
```

### Text [#text]

`text` supplies the visible content for all four text variants. It is separate from the accessible status label.

```tsx
<Loading variant="text-dots" text="Uploading" aria-label="Uploading file" />
<Loading variant="text-blink" text="Thinking" variantProps={{ minOpacity: 0.3 }} />
<Loading variant="text-shimmer" text="Generating" variantProps={{ duration: 2, spread: 3 }} />
<Loading variant="text-shimmer-wave" text="Processing" variantProps={{ yDistance: -4 }} />
```

### Animation Timing [#animation-timing]

`duration` sets the inherited `--duration` CSS variable in seconds for CSS-based loaders. Original fixed Motion and SVG SMIL timings remain unchanged, including `spiral`, `morphing-infinity`, `analyzing-image`, `diamond`, `dash-ring`, and `ripple`. For Motion loaders that expose a timing prop, use `variantProps.duration`:

```tsx
<Loading variant="ring" duration={0.8} />
<Loading variant="bobbing-dots" variantProps={{ duration: 1.5 }} />
<Loading variant="pulsating-dots" variantProps={{ duration: 1.5 }} />
```

## Optional Storybook Stories [#optional-storybook-stories]

Install Loading first, then run `npx shadcn@latest add https://vandor-ui.vercel.app/r/loading-stories.json`. Stories cover all 47 variants, text, CSS sizes, variant-specific options, and an accessible busy region. The Playground variant control is the lowest-cost way to inspect one animation at a time; AllVariants runs the whole catalog. Change your OS/browser reduced-motion preference to inspect the static fallback, which is not a component prop.

Stories install beside Loading under your configured UI alias and use relative imports. Keep Loading's installed animation snapshot and license. Use your existing matching `@storybook/react` packages (verified with 10.6.1), discover `**/*.stories.tsx`, and load your global Tailwind/theme stylesheet. No addons, extra packages, core overwrites, or Storybook config edits are required. Storybook UI is not verified because no server exists here.

## Accessibility [#accessibility]

The wrapper is a `div` with `role="status"` and the default accessible name `Loading`. Native div props and its ref are forwarded to the wrapper. All animated visuals are hidden from assistive technology to avoid duplicate announcements.

Provide a localized `aria-label` describing the operation. For a decorative indicator next to an existing status message, set `aria-hidden` on `Loading`. Render the loader only while work is pending, and use `aria-busy` on the region being updated when appropriate.

When `prefers-reduced-motion` is enabled, looping animations are replaced by a static ring, or static content for text variants. The accessible status name is preserved.

## Props [#props]

| Prop         | Type                                   | Default          | Description                                                                                                                                                                                    |
| ------------ | -------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| variant      | LoadingVariant                         | "arc"            | Select any of the 47 loading-ui components. Names match the upstream registry.                                                                                                                 |
| aria-label   | string                                 | "Loading"        | Accessible status name. Translate or customize for the current operation. Decorative use can set aria-hidden on the wrapper.                                                                   |
| className    | string                                 | Not set          | Layout and inherited color classes on the wrapper. Use variantProps.className to style the visual itself.                                                                                      |
| duration     | number                                 | Upstream default | CSS animation cycle in seconds, via --duration. Fixed Motion and SVG SMIL timings are unchanged. For text shimmer, bobbing dots, and pulsating dots, use variantProps.duration.                |
| size         | number \| string                       | 24               | Base visual size. Text and glyph variants use this as font size; skeleton and eyes use proportional dimensions. The playground starts at 32px.                                                 |
| text         | string                                 | "Loading"        | Visible content for text-blink, text-dots, text-shimmer, and text-shimmer-wave.                                                                                                                |
| variantProps | Props of selected loading-ui component | Not set          | Typed upstream props for the selected variant, including dots, bars, prompt, glyphs, eye scales, colors, className, and style. Children are supplied through text; refs belong to the wrapper. |

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

## Source [#source]

```tsx
"use client";

import { cn } from "cn";
import { useReducedMotion } from "motion/react";
import type { ComponentProps, ComponentType, CSSProperties } from "react";

import { AccordionLoader } from "@/components/loading-ui/accordion-loader";
import { AnalyzingImage } from "@/components/loading-ui/analyzing-image";
import { Arc } from "@/components/loading-ui/arc";
import { Bars } from "@/components/loading-ui/bars";
import { BobbingDots } from "@/components/loading-ui/bobbing-dots";
import { BouncingDots } from "@/components/loading-ui/bouncing-dots";
import { Classic } from "@/components/loading-ui/classic";
import { ClockRing } from "@/components/loading-ui/clock-ring";
import { CometSpinner } from "@/components/loading-ui/comet-spinner";
import { ConcentricRing } from "@/components/loading-ui/concentric-ring";
import { ConveyorLoop } from "@/components/loading-ui/conveyor-loop";
import { DashRing } from "@/components/loading-ui/dash-ring";
import { Diamond } from "@/components/loading-ui/diamond";
import { Dots } from "@/components/loading-ui/dots";
import { DotsRing } from "@/components/loading-ui/dots-ring";
import { DualArc } from "@/components/loading-ui/dual-arc";
import { FadeArc } from "@/components/loading-ui/fade-arc";
import { InfinityLoop } from "@/components/loading-ui/infinity";
import { InfinitySquareSnake } from "@/components/loading-ui/infinity-square-snake";
import { InfinityTrack } from "@/components/loading-ui/infinity-track";
import { MorphingInfinity } from "@/components/loading-ui/morphing-infinity";
import { OrbitRing } from "@/components/loading-ui/orbit-ring";
import { PulsatingDots } from "@/components/loading-ui/pulsating-dots";
import { Pulse } from "@/components/loading-ui/pulse";
import { PulseDot } from "@/components/loading-ui/pulse-dot";
import { QuarterRing } from "@/components/loading-ui/quarter-ring";
import { Ring } from "@/components/loading-ui/ring";
import { Ripple } from "@/components/loading-ui/ripple";
import { SatelliteRing } from "@/components/loading-ui/satellite-ring";
import { Skeleton } from "@/components/loading-ui/skeleton";
import { Spiral } from "@/components/loading-ui/spiral";
import { Spokes } from "@/components/loading-ui/spokes";
import { SquareAccordion } from "@/components/loading-ui/square-accordion";
import { SquareGrid } from "@/components/loading-ui/square-grid";
import { SquareSnake } from "@/components/loading-ui/square-snake";
import { Swirling } from "@/components/loading-ui/swirling";
import { SymmetricWave } from "@/components/loading-ui/symmetric-wave";
import { Terminal } from "@/components/loading-ui/terminal";
import { TextBlink } from "@/components/loading-ui/text-blink";
import { TextDots } from "@/components/loading-ui/text-dots";
import { TextShimmer } from "@/components/loading-ui/text-shimmer";
import { TextShimmerWave } from "@/components/loading-ui/text-shimmer-wave";
import { TripleDotSpinner } from "@/components/loading-ui/triple-dot-spinner";
import { TwinOrbit } from "@/components/loading-ui/twin-orbit";
import { Typing } from "@/components/loading-ui/typing";
import { WanderingEyes } from "@/components/loading-ui/wandering-eyes";
import { Wave } from "@/components/loading-ui/wave";

import { LoadingArc } from "./loading-arc";
import type { LoadingVariant } from "./loading-variants";
export { loadingVariants, type LoadingVariant } from "./loading-variants";

// Original animations: https://github.com/turbostarter/loading-ui (MIT).
// The registry includes a versioned snapshot as editable source.
const loadingComponents = {
  "accordion-loader": AccordionLoader,
  "analyzing-image": AnalyzingImage,
  arc: Arc,
  bars: Bars,
  "bobbing-dots": BobbingDots,
  "bouncing-dots": BouncingDots,
  classic: Classic,
  "clock-ring": ClockRing,
  "comet-spinner": CometSpinner,
  "concentric-ring": ConcentricRing,
  "conveyor-loop": ConveyorLoop,
  "dash-ring": DashRing,
  diamond: Diamond,
  dots: Dots,
  "dots-ring": DotsRing,
  "dual-arc": DualArc,
  "fade-arc": FadeArc,
  infinity: InfinityLoop,
  "infinity-square-snake": InfinitySquareSnake,
  "infinity-track": InfinityTrack,
  "morphing-infinity": MorphingInfinity,
  "orbit-ring": OrbitRing,
  "pulsating-dots": PulsatingDots,
  pulse: Pulse,
  "pulse-dot": PulseDot,
  "quarter-ring": QuarterRing,
  ring: Ring,
  ripple: Ripple,
  "satellite-ring": SatelliteRing,
  skeleton: Skeleton,
  spiral: Spiral,
  spokes: Spokes,
  "square-accordion": SquareAccordion,
  "square-grid": SquareGrid,
  "square-snake": SquareSnake,
  swirling: Swirling,
  "symmetric-wave": SymmetricWave,
  terminal: Terminal,
  "text-blink": TextBlink,
  "text-dots": TextDots,
  "text-shimmer": TextShimmer,
  "text-shimmer-wave": TextShimmerWave,
  "triple-dot-spinner": TripleDotSpinner,
  "twin-orbit": TwinOrbit,
  typing: Typing,
  "wandering-eyes": WanderingEyes,
  wave: Wave,
} as const satisfies Record<LoadingVariant, unknown>;

type LoadingBaseProps = Omit<ComponentProps<"div">, "children"> & {
  /** Base visual size in pixels, or a CSS length. Glyph/text variants use it as font size. */
  size?: number | string;
  /** CSS-based cycle duration in seconds. Fixed Motion/SMIL timings remain upstream defaults. */
  duration?: number;
  /** Visible content for the four text variants. */
  text?: string;
};

type SelectedLoadingProps = LoadingBaseProps &
  {
    [Variant in LoadingVariant]: {
      variant: Variant;
      variantProps?: Omit<
        ComponentProps<(typeof loadingComponents)[Variant]>,
        "children" | "ref"
      >;
    };
  }[LoadingVariant];

type DefaultLoadingProps = LoadingBaseProps & {
  variant?: "arc";
  variantProps?: Omit<ComponentProps<typeof Arc>, "children" | "ref">;
};

export type LoadingProps =
  | SelectedLoadingProps
  | DefaultLoadingProps
  | (LoadingBaseProps & {
      variant: LoadingVariant;
      variantProps?: never;
    });

const glyphVariants = new Set<LoadingVariant>([
  "accordion-loader",
  "conveyor-loop",
  "infinity-square-snake",
  "infinity-track",
  "square-accordion",
  "square-grid",
  "square-snake",
  "symmetric-wave",
  "terminal",
]);

const visualClassName = (variant: LoadingVariant) => {
  if (variant.startsWith("text-") || glyphVariants.has(variant)) {
    return "text-[length:var(--loading-size)]";
  }
  if (variant === "skeleton") {
    return "h-[calc(var(--loading-size)/2)] w-[calc(var(--loading-size)*4)]";
  }
  if (variant === "wandering-eyes") {
    return "h-(--loading-size) w-[calc(var(--loading-size)*2.25)]";
  }
  if (variant === "twin-orbit" || variant === "triple-dot-spinner") {
    return "size-[calc(var(--loading-size)/3)]";
  }
  return "size-(--loading-size)";
};

const StaticLoading = ({ isText, text }: { isText: boolean; text: string }) => {
  if (isText) {
    return <span className="text-[length:var(--loading-size)]">{text}</span>;
  }
  return (
    <svg viewBox="0 0 24 24" fill="none" className="size-(--loading-size)">
      <circle
        cx="12"
        cy="12"
        r="9"
        stroke="currentColor"
        strokeWidth="2"
        opacity="0.3"
      />
      <path
        d="M12 3a9 9 0 0 1 9 9"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
      />
    </svg>
  );
};

export const Loading = ({
  variant = "arc",
  variantProps,
  size = 24,
  duration,
  text = "Loading",
  className,
  style,
  "aria-label": ariaLabel = "Loading",
  ...props
}: LoadingProps) => {
  const reducedMotion = useReducedMotion();
  if (variant === "arc" && !variantProps) {
    return (
      <LoadingArc
        {...props}
        size={size}
        className={className}
        aria-label={ariaLabel}
        style={
          {
            ...(duration === undefined ? {} : { "--duration": `${duration}s` }),
            ...style,
          } as CSSProperties
        }
      />
    );
  }
  // The public discriminated union validates each variant's own props. The common
  // rendering boundary only adds className and string children to text components.
  const Component = loadingComponents[variant] as ComponentType<{
    className?: string;
    children?: string;
  }>;
  const isText = variant.startsWith("text-");
  return (
    <div
      role="status"
      aria-label={ariaLabel}
      data-slot="loading"
      data-variant={variant}
      className={cn(
        "inline-flex shrink-0 items-center justify-center align-middle",
        className
      )}
      style={
        {
          "--loading-size": typeof size === "number" ? `${size}px` : size,
          ...(duration === undefined ? {} : { "--duration": `${duration}s` }),
          ...style,
        } as CSSProperties
      }
      {...props}
    >
      <div
        aria-hidden="true"
        data-slot="loading-visual"
        className="inline-flex items-center justify-center"
      >
        {reducedMotion ? (
          <StaticLoading isText={isText} text={text} />
        ) : (
          <Component
            {...(isText && variant !== "text-dots" ? { as: "span" } : {})}
            {...variantProps}
            {...(isText ? { children: text } : {})}
            className={cn(visualClassName(variant), variantProps?.className)}
          />
        )}
      </div>
    </div>
  );
};

```

## Credits [#credits]

* [loading-ui](https://github.com/turbostarter/loading-ui): Original MIT-licensed animations by Bartosz Zagrodzki. Vandor UI adds a unified typed variant API, proportional sizing, accessible status labels, and a reduced-motion fallback.
