f-ui
Components

Spin

Indeterminate loading indicator with optional tip, nested overlay, and fullscreen app lock (Ant Spin class).

Spin is the process indicator for waits whose layout is unknown, a region refresh overlay, or a whole-app lock. It follows Ant Design Spin (spinning, tip, nested children, fullscreen). It is not a Loading status-block peer to Empty / Result. List / Detail Hub / Form first paint still uses a page-shaped Skeleton — see Page And Region Status.

When To Use

  • Show an honest “working” wait when the destination layout is unknown (bootstrap, session hydrate, route module pending).
  • Wrap a region with nested Spin so stale content stays visible under a veil during a refresh.
  • Use fullscreen only when the whole app is locked — not as the CRUD page default.
  • Prefer Skeleton for List / Detail Hub / Form hydrate when the shape is known.
  • Prefer Progress when the job has a measurable percent.

Features

AreaBehavior
IndicatorDefault Spinner at small / default / large; override with indicator
TipOptional sentence-case label under the indicator
Nestedchildren stay mounted; a 45% veil + indicator covers them while spinning
Fullscreenfullscreen pins an opaque bg-background shell to the viewport (z-50)
A11yRoot (or mask) is role="status" aria-busy; the SVG is presentation-only
SlotsclassNames for root · indicator · tip · mask

Installing

pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/spin.json
npx shadcn@latest add https://ui.isaacfei.com/r/spin.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/spin.json
bun x shadcn@latest add https://ui.isaacfei.com/r/spin.json

With a namespace: npx shadcn@latest add @f-ui/spin.

registryDependencies: shadcn spinner, utils.

Usage

import { Spin } from "@/components/f-ui/spin/spin";

<Spin size="large" tip="Loading" fullscreen />

Examples

Centered Wait

Standalone Spin in a region band — spinner plus a short tip. Use this when the content shape is unknown; do not use it as a List / Hub / Form first paint.

Loading

"use client";

import { Spin } from "@/components/f-ui/spin/spin";

export function SpinDemo() {
  return (
    <div className="bg-muted/40 flex min-h-48 items-center justify-center rounded-xl border">
      <Spin size="large" tip="Loading" />
    </div>
  );
}

Nested Overlay

Wrap existing content. The region stays; Spin veils it while spinning is true.

Orders

Nested Spin keeps the region shape and veils the content while a refresh is in flight.

Refreshing

"use client";

import { useState } from "react";

import { Spin } from "@/components/f-ui/spin/spin";
import { Button } from "@/components/ui/button";

export function SpinNestedDemo() {
  const [spinning, setSpinning] = useState(true);

  return (
    <div className="flex flex-col gap-3">
      <Button type="button" variant="outline" onClick={() => setSpinning((v) => !v)}>
        {spinning ? "Stop" : "Spin"}
      </Button>
      <Spin spinning={spinning} tip="Refreshing">
        <div className="bg-card space-y-3 rounded-xl border p-4">
          <p className="text-sm font-medium">Orders</p>
          <p className="text-muted-foreground text-sm">
            Nested Spin keeps the region shape and veils the content while a refresh
            is in flight.
          </p>
        </div>
      </Spin>
    </div>
  );
}

App Lock

fullscreen covers the viewport with an opaque shell — bootstrap, session gate, or a blocking lock. The preview below is contained; Show app lock runs the real overlay for two seconds.

Loading

"use client";

import { useEffect, useState } from "react";

import { Spin } from "@/components/f-ui/spin/spin";
import { Button } from "@/components/ui/button";

export function SpinFullscreenDemo() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (!open) return;
    const id = window.setTimeout(() => setOpen(false), 2000);
    return () => window.clearTimeout(id);
  }, [open]);

  return (
    <div className="flex flex-col gap-3">
      <Button type="button" onClick={() => setOpen(true)}>
        Show app lock
      </Button>
      <div className="relative h-64 overflow-hidden rounded-xl border">
        <Spin
          size="large"
          tip="Loading"
          className="absolute inset-0 bg-background"
        />
      </div>
      {open ? <Spin fullscreen size="large" tip="Loading" /> : null}
    </div>
  );
}

Composition

Spin
├── (no children) root — centered chrome, optional fullscreen shell
│   ├── Spinner (ui) | indicator
│   └── tip?
└── (children) relative root
    ├── children
    └── mask? — veil + chrome while spinning

API Reference

Props

PropTypeDefaultDescription
spinningbooleantrueShow the indicator (and overlay when wrapping children).
size"small" | "default" | "large""default"Indicator size (size-4 / size-6 / size-8).
tipReactNodeLabel under the indicator.
indicatorReactNodedefault SpinnerReplace the built-in spinner.
fullscreenbooleanfalseViewport app-lock shell. Ignored when children is set.
childrenReactNodeNested content to veil while spinning.
classNamestringMerged onto the root.
classNamesPartial<Record<SpinSlot, string>>Per-slot classes.

Slots

SlotApplied to
rootOutermost wrapper
indicatorDefault Spinner
tipVisible label
maskNested overlay veil

On this page