Spinner
Loading indicators with multiple animation styles, semantic colors, and configurable speed.
Playground
<Spinner
label="Loading..."
visible
/>01 Examples
In a list
- Item 1 — Loaded
- Item 2 — Loaded
- Item 3 — Loaded
<div
class="border-border-subtle bg-surface-elevated divide-border-subtle w-full divide-y rounded-2xl border"
>
<div class="flex items-center gap-3 px-4 py-3">
<span class="text-text-primary flex-1 text-sm">design-tokens.md</span>
<Badge intent="success" size="sm" variant="soft">Synced</Badge>
</div>
<div class="flex items-center gap-3 px-4 py-3">
<span class="text-text-primary flex-1 text-sm">components.tsx</span>
<Spinner size="sm" intent="primary" label="Syncing components.tsx" />
</div>
<div class="flex items-center gap-3 px-4 py-3">
<span class="text-text-primary flex-1 text-sm">accessibility-audit.md</span>
<Badge intent="neutral" size="sm" variant="soft">Queued</Badge>
</div>
</div>
<div class="border-border-subtle bg-surface-elevated w-full rounded-2xl border p-4">
<ul class="space-y-2.5">
<li class="text-text-primary text-sm">Item 1 — Loaded</li>
<li class="text-text-primary text-sm">Item 2 — Loaded</li>
<li class="text-text-primary text-sm">Item 3 — Loaded</li>
</ul>
<div class="border-border-subtle mt-4 flex items-center justify-center gap-2 border-t pt-4">
<Spinner size="sm" variant="dots" intent="primary" />
<span class="text-text-tertiary text-xs">Loading more…</span>
</div>
</div>Color inheritance
intent='current' so the spinner picks up the surrounding text color via currentColor — ideal inside coloured surfaces, badges, or alerts where a fixed intent would clash.<div
class="text-success-emphasis bg-success/15 inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium"
>
<Spinner size="xs" intent="current" />
Deploying
</div>
<div
class="text-warning-emphasis bg-warning/15 inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium"
>
<Spinner size="xs" intent="current" />
Retrying
</div>
<div
class="text-danger-emphasis bg-danger/15 inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium"
>
<Spinner size="xs" intent="current" />
Rolling back
</div>Content Loading Overlay
Loading content…
<div class="relative w-full max-w-sm overflow-hidden">
<Card padding="lg">
<div class="space-y-3 opacity-30 select-none" aria-hidden="true">
<div class="bg-surface-subtle rounded-modify h-4 w-3/4"></div>
<div class="bg-surface-subtle rounded-modify h-4 w-full"></div>
<div class="bg-surface-subtle rounded-modify h-4 w-5/6"></div>
<div class="bg-surface-subtle rounded-modify mt-4 h-8 w-1/3"></div>
</div>
</Card>
<div
class="bg-surface-base/60 rounded-contain absolute inset-0 z-[var(--z-docked)] flex flex-col items-center justify-center gap-3 backdrop-blur-sm"
>
<Spinner size="lg" intent="primary" variant="ring" />
<p class="text-text-secondary text-sm font-medium">Loading content…</p>
</div>
</div>Status Pipeline
<div class="flex w-full max-w-md flex-col gap-4">
<div class="flex items-center gap-3">
<div
class="bg-success/15 text-success flex h-8 w-8 items-center justify-center rounded-full"
>
<CheckIcon size={16} />
</div>
<span class="text-text-secondary text-sm">Files uploaded</span>
<Badge intent="success" size="sm" variant="soft" class="ml-auto">Done</Badge>
</div>
<div class="flex items-center gap-3">
<Spinner size="lg" intent="primary" variant="bars" speed="fast" />
<span class="text-text-primary text-sm font-medium">Processing data…</span>
<Badge intent="primary" size="sm" variant="soft" pulse class="ml-auto">Active</Badge>
</div>
<div class="flex items-center gap-3">
<div
class="bg-surface-subtle text-text-tertiary flex h-8 w-8 items-center justify-center rounded-full"
>
<span class="text-xs font-bold">3</span>
</div>
<span class="text-text-tertiary text-sm">Deploy to production</span>
<Badge intent="neutral" size="sm" variant="soft" class="ml-auto">Pending</Badge>
</div>
</div>02 Customization
Slot Overrides
<Spinner
size="xl"
slotClasses={{
svgCircle: 'opacity-10',
svgPath: 'fill-current drop-shadow-[0_0_6px_currentColor]'
}}
/>
<Spinner variant="dots" size="xl" intent="success" slotClasses={{ dot: 'rounded-none' }} />
<Spinner variant="bars" size="xl" intent="danger" slotClasses={{ bar: 'rounded-full' }} />Fully Custom (unstyled)
<Spinner
unstyled
class="relative inline-flex h-10 w-10 items-center justify-center"
slotClasses={{
svg: 'w-full h-full animate-spin [animation-duration:0.8s]',
svgCircle: 'opacity-0',
svgPath: 'fill-sky-400 drop-shadow-[0_0_8px_rgba(56,189,248,0.5)]'
}}
label="Syncing"
/>
<Spinner
variant="dots"
unstyled
class="inline-flex h-10 items-center gap-2"
slotClasses={{
dots: 'flex items-center gap-2',
dot: 'h-2.5 w-2.5 rounded-full bg-emerald-400 animate-bounce [animation-duration:0.8s] shadow-[0_0_8px_rgba(52,211,153,0.5)]'
}}
label="Connecting"
/>
<Spinner
variant="bars"
unstyled
class="inline-flex h-10 items-center"
slotClasses={{
bars: 'flex items-center gap-1',
bar: 'w-1 h-7 rounded-full bg-violet-400 [animation-name:spinner-bars] [animation-duration:0.7s] [animation-timing-function:ease-in-out] [animation-iteration-count:infinite] [&:nth-child(1)]:delay-[-0.36s] [&:nth-child(2)]:delay-[-0.24s] [&:nth-child(3)]:delay-[-0.12s] shadow-[0_0_8px_rgba(167,139,250,0.5)]'
}}
label="Analyzing"
/>A branded loader used across the app belongs in a BlocksProvider preset (presets.Spinner), applied via preset — see Customization.
03 Accessibility
Screen Reader
Uses role="status" with aria-label from the label prop (defaults to "Loading…"). The aria-live="polite" region announces loading state without
interrupting the user.
Visibility
When visible={false}, the spinner is removed from
the DOM entirely — no visual output and no accessibility tree entry.
Reduced Motion
All animations (spin, bounce, pulse, bars) respect prefers-reduced-motion: reduce via Tailwind's motion-reduce: variant. In reduced-motion mode the spinner
still renders statically to indicate loading.
Spinners are hidden from print output via a @media print rule.
04 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
children | Snippet | — | Optional content rendered beside the spinner (e.g. loading text). | |
class | string | — | Extra classes merged onto the root element. | |
intent | SpinnerVariants['intent'] | — | Semantic color applied via text-* token. | |
label | string | — | Accessible label announced by screen readers. Defaults to "Loading...". | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Spinner: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
size | SpinnerVariants['size'] | — | Physical dimensions from xs (16 px) to xl (40 px). | |
slotClasses | Partial<Record<SpinnerSlots, string>> | — | Per-slot class overrides merged with (or replacing, when unstyled) tv() output.
Slots: base | svg | svgCircle | svgPath | dots | dot | pulse | pulseCenter |
pulseRing | ring | ringElement | bars | bar | content | srOnly | |
speed | SpinnerVariants['speed'] | — | Animation speed — controls --spinner-speed custom property. | |
unstyled | boolean | — | Strip all default tv() classes. Combine with slotClasses for full control. | |
variant | SpinnerVariants['variant'] | — | Animation style. default is an SVG arc, dots bounces three dots,
pulse radiates a sonar ping, ring spins cascading borders,
bars animates vertical equalizer bars. | |
visible | boolean | — | When false the spinner is removed from the DOM. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children') | |
...SpinnerVariants variant | VariantProps | — | Styling variants from SpinnerVariants |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
SpinnerProps | interface | props | 0 | — | |
SpinnerVariants | type | variant | 1 | — | |
SpinnerSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. |
06 Installation
Import
import { Spinner } from '@urbicon-ui/blocks';