Skip to main content

Overview

The Spinner widget displays an animated loading indicator to provide visual feedback during asynchronous operations. It supports multiple animation variants and optional labels.

Usage

import { ui } from "@rezi-ui/core";

// Basic spinner
ui.spinner()

// With label
ui.spinner({ label: "Loading..." })

// Different variant
ui.spinner({ variant: "dots", label: "Processing" })

Props

variant
string
default:"dots"
Spinner animation variant:
  • "dots" - Animated dots sequence
  • "line" - Horizontal line animation
  • "circle" - Spinning circle
  • "bounce" - Bouncing animation
  • "pulse" - Pulsing effect
  • "arrows" - Rotating arrows
  • "dots2" - Alternative dots pattern
label
string
Optional text label displayed after the spinner
style
TextStyle
Optional style override for the spinner and label
key
string
Optional reconciliation key for efficient updates

Examples

Basic Loading State

function LoadingView() {
  return ui.column({ gap: 1, p: 1 }, [
    ui.spinner({ label: "Loading data..." }),
  ]);
}

Multiple Spinner Variants

function SpinnerShowcase() {
  const variants = ["dots", "line", "circle", "bounce", "pulse", "arrows"] as const;
  
  return ui.column({ gap: 1, p: 1 }, [
    ui.text("Spinner Variants", { variant: "heading" }),
    ...variants.map((variant) =>
      ui.row({ gap: 1, key: variant }, [
        ui.spinner({ variant }),
        ui.text(variant),
      ])
    ),
  ]);
}

Conditional Loading

type State = {
  loading: boolean;
  data: string | null;
};

function DataView(state: State) {
  if (state.loading) {
    return ui.center(
      ui.spinner({ variant: "circle", label: "Fetching data..." })
    );
  }
  
  return ui.text(state.data ?? "No data");
}

Styled Spinner

import { rgb } from "@rezi-ui/core";

function CustomSpinner() {
  return ui.spinner({
    variant: "circle",
    label: "Please wait",
    style: {
      fg: rgb(100, 200, 255),
      bold: true,
    },
  });
}

Loading State Patterns

Inline Loading

function InlineLoadingButton(state: { saving: boolean }) {
  return ui.row({ gap: 1, items: "center" }, [
    ui.button({
      id: "save-btn",
      label: "Save",
      disabled: state.saving,
    }),
    state.saving && ui.spinner({ variant: "dots" }),
  ]);
}

Full-Screen Overlay

function LoadingOverlay() {
  return ui.layers([
    MainContent(),
    ui.box(
      {
        width: "100%",
        height: "100%",
        style: { bg: rgb(0, 0, 0, 0.5) },
      },
      [
        ui.center(
          ui.column({ gap: 1 }, [
            ui.spinner({ variant: "circle" }),
            ui.text("Loading...", { style: { bold: true } }),
          ])
        ),
      ]
    ),
  ]);
}

Section Loading

function DataSection(state: { loading: boolean; items: Item[] }) {
  return ui.panel("Recent Activity", [
    state.loading
      ? ui.column({ gap: 1, p: 2 }, [
          ui.spinner({ label: "Loading activity..." }),
        ])
      : ui.column(
          { gap: 0 },
          state.items.map((item, i) => ui.text(item.name, { key: String(i) }))
        ),
  ]);
}

Animation Behavior

The spinner animation is driven by the framework’s tick events:
  • Animation updates on every frame when the app is rendering
  • Pauses automatically when the terminal is resized or suspended
  • No manual animation state management required
Spinners automatically animate as long as they remain mounted. Remove the spinner from the view tree when loading completes.

Design System Integration

Spinners inherit theme colors by default:
// Uses theme accent color
ui.spinner({ label: "Loading" })

// Override with custom color
ui.spinner({
  label: "Custom",
  style: { fg: rgb(255, 100, 50) },
})

Accessibility

When using spinners for loading states:
  1. Provide context - Include a descriptive label explaining what’s loading
  2. Time limits - Show alternative UI if loading exceeds expected duration
  3. Cancellation - Provide a way to cancel long-running operations
function AccessibleLoading(state: { elapsed: number }) {
  return ui.column({ gap: 1 }, [
    ui.spinner({ label: "Loading data..." }),
    state.elapsed > 5000 &&
      ui.text("This is taking longer than usual", { style: { dim: true } }),
    state.elapsed > 10000 &&
      ui.button({
        id: "cancel-load",
        label: "Cancel",
        intent: "secondary",
      }),
  ]);
}

Best Practices

Use appropriate variants

Choose variants that match the operation duration:
  • Quick operations: "dots" or "pulse"
  • Long operations: "circle" or "bounce"

Add descriptive labels

Always include a label explaining what’s loading to provide context to users.

Position strategically

Place spinners near the content being loaded rather than at arbitrary locations.

Remove when done

Ensure spinners are removed from the view tree when operations complete to avoid confusing users.
  • Progress - For operations with known completion percentage
  • Skeleton - For placeholder loading states
  • Callout - For status messages after loading completes

Build docs developers (and LLMs) love