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
fullscreenonly 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
| Area | Behavior |
|---|---|
| Indicator | Default Spinner at small / default / large; override with indicator |
| Tip | Optional sentence-case label under the indicator |
| Nested | children stay mounted; a 45% veil + indicator covers them while spinning |
| Fullscreen | fullscreen pins an opaque bg-background shell to the viewport (z-50) |
| A11y | Root (or mask) is role="status" aria-busy; the SVG is presentation-only |
| Slots | classNames for root · indicator · tip · mask |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/spin.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/spin.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/spin.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/spin.jsonWith 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 spinningAPI Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
spinning | boolean | true | Show the indicator (and overlay when wrapping children). |
size | "small" | "default" | "large" | "default" | Indicator size (size-4 / size-6 / size-8). |
tip | ReactNode | — | Label under the indicator. |
indicator | ReactNode | default Spinner | Replace the built-in spinner. |
fullscreen | boolean | false | Viewport app-lock shell. Ignored when children is set. |
children | ReactNode | — | Nested content to veil while spinning. |
className | string | — | Merged onto the root. |
classNames | Partial<Record<SpinSlot, string>> | — | Per-slot classes. |
Slots
| Slot | Applied to |
|---|---|
root | Outermost wrapper |
indicator | Default Spinner |
tip | Visible label |
mask | Nested overlay veil |