Skip to content
LofiStack Gallery
Type: loaderWeek 01

Orbit Loader

An SVG loader with up to three counter-rotating orbits. Leave progress empty for an indeterminate spinner, or pass 0–100 and the outer orbit becomes a progress ring with a live percentage. It inherits text color, so it works on any surface and in both themes.

View on GitHub
Loading

Usage

usage.tsx
import { OrbitLoader } from "@/components/gallery/orbit-loader";

// Indeterminate — inherits the text color
<OrbitLoader className="text-indigo-500" label="Fetching orders" />

// Determinate — outer orbit becomes a progress ring
<OrbitLoader size="lg" progress={uploaded} label="Uploading" showLabel />

// Custom color, 2 orbits, slower, paused while queued
<OrbitLoader orbits={2} speed={2.4} color="#f59e0b" paused={isQueued} />

States

Indeterminate (1, 2 or 3 orbits) · Determinate progress 0–100% · Paused · Sizes: sm, md, lg, custom px · Reduced motion (4× slower)

Loading
1 orbit
Loading
2 orbits
Loading
3 orbits
Loading · 35%
Progress 35%
Loading
Paused
Syncing
Small + label

Props

  • size= "md"

    "sm" | "md" | "lg" | number

    Preset (32 / 48 / 72px) or an exact pixel size.

  • orbits= 3

    1 | 2 | 3

    How many concentric orbits to render.

  • speed= 1.6

    number

    Seconds per revolution of the outer orbit. Inner orbits run progressively slower.

  • color= "currentColor"

    string

    Arc and head color. Any CSS color.

  • trackColor= color

    string

    Track color, rendered at 15% opacity.

  • progress

    number

    0–100. Switches the outer orbit to a determinate ring and the role to progressbar.

  • label= "Loading"

    string

    Accessible name. Also the visible caption when showLabel is on.

  • showLabel= false

    boolean

    Show the caption (and percentage) under the loader.

  • paused= false

    boolean

    Freeze the animation without unmounting.

  • className

    string

    Extra classes for the wrapper — e.g. a text color.

Accessibility

  • Indeterminate mode uses role="status" with a polite live region and a screen-reader label.
  • Determinate mode uses role="progressbar" with aria-valuenow / min / max.
  • The SVG is aria-hidden; the label carries the meaning.
  • prefers-reduced-motion slows the rotation 4× instead of removing the feedback.

Build prompt

The final prompt used to build this component
Build a reusable React + TypeScript + Tailwind loader component called OrbitLoader.

Visual: an SVG (viewBox 100x100) with up to 3 concentric orbits (radii 44, 31, 18). Each orbit has a faint track (15% opacity) and a rounded arc segment with a solid "head" dot at its leading edge. Orbits rotate at different speeds and alternate direction so it feels like a small solar system.

Props (typed interface): size ("sm" | "md" | "lg" | number px), orbits (1–3), speed (seconds per outer revolution), color (default currentColor so it inherits text color), trackColor, progress (0–100), label, showLabel, paused, className.

Behaviour:
- No progress → indeterminate: role="status", aria-live polite, sr-only label.
- progress given → the outer orbit becomes a determinate progress ring (stroke-dashoffset, smooth transition) with the head dot at the end of the progress; role="progressbar" with aria-valuenow/min/max; caption shows "Label · 42%".
- paused freezes animation via animation-play-state.
- prefers-reduced-motion slows the animation 4x instead of removing it.
- Keep it self-contained: keyframes ship with the component via a React 19 <style href precedence> tag, no global CSS or dependencies.
- Clamp progress to 0–100 and orbits to 1–3. No hardcoded colors or sizes.