Animated braille-glyph loading spinners for React. 19 unique animations, zero dependencies, fully accessible. — Live demo → · GitHub →
cli-loaders gives you 19 animated braille-glyph spinners as a set of composable, accessible React components. Zero runtime dependencies beyond React itself.
- 19 spinner animations with unique braille Unicode patterns
- Zero runtime dependencies — pure React, no external libraries
- Fully accessible — WCAG 2.1 AA,
prefers-reduced-motionsupport, semantic HTML - TypeScript first — complete type definitions,
DotShapeexport for custom rendering - Composable — 7 components + 2 hooks for any use case
- Small bundle — ~15KB gzipped (lib) + optional SVG rendering
- Shape customization — render as circles, squares, or diamonds (SVG mode)
vs. react-spinners: Smaller bundle, braille Unicode art for terminal-like aesthetics, context API for global defaults, shape customization (circles/squares/diamonds)
vs. react-loading: Zero dependencies, comprehensive accessibility testing, 19+ unique animations, SVG rendering mode
vs. custom CSS: No @keyframes to maintain, out-of-the-box reduced-motion support, semantic HTML, accessible by default
npm install @agilek/cli-loaders
# or
pnpm add @agilek/cli-loaders
# or
yarn add @agilek/cli-loadersRequires React ≥ 18.
import { Spinner } from '@agilek/cli-loaders';
// Minimal — defaults to the "braille" spinner
<Spinner />
// Named + coloured
<Spinner name="helix" color="#7c3aed" size="1.5rem" />
// With custom dot shape (renders as SVG instead of braille text)
<Spinner name="scan" color="#00ff99" shape="square" size="2rem" />The base animated glyph. Renders as an inline <span> so it drops naturally into any line of text.
| Prop | Type | Default | Description |
|---|---|---|---|
name |
SpinnerName |
"braille" |
Which spinner animation to use |
color |
string |
currentColor |
CSS color value |
size |
string | number |
undefined |
Font size of the glyph, e.g. "1.5rem" or 24 |
speed |
number |
1 |
Playback multiplier — 2 = twice as fast |
paused |
boolean |
false |
Freeze the animation |
ignoreReducedMotion |
boolean |
false |
Override prefers-reduced-motion and always animate |
shape |
"circle" | "square" | "diamond" |
undefined |
Render dots as SVG shapes instead of braille text |
label |
string |
"Loading" |
Accessible label announced by screen readers |
className |
string |
— | Applied to the outer <span> |
style |
CSSProperties |
— | Inline styles for the outer <span> |
ref |
Ref<HTMLSpanElement> |
— | Forwarded to the outer <span> |
Any additional HTML span attributes are forwarded.
Spinner + children in a flex row — handy for inline status messages.
<SpinnerInline name="braille" color="#00ff99">
Fetching data…
</SpinnerInline>Accepts all Spinner props plus:
| Prop | Type | Default | Description |
|---|---|---|---|
gap |
string | number |
"0.4em" |
Gap between spinner and children |
children |
ReactNode |
— | Content rendered after the spinner |
Decorates a text string with spinners — supports a bookend mode that mirrors one on each end.
<SpinnerText text="Deploying" color="#f59e0b" bookend />
// ⠿ Deploying ⠿| Prop | Type | Default | Description |
|---|---|---|---|
text |
string |
required | The text to decorate |
bookend |
boolean |
false |
Place a spinner on both sides |
gap |
string | number |
"0.4em" |
Gap between spinners and text |
Plus all Spinner props.
A tinted pill badge with a spinner — good for live status indicators.
<SpinnerBadge label="Live" color="#ef4444" />
<SpinnerBadge label="Syncing" color="#38bdf8" />
<SpinnerBadge label="Building" color="#f59e0b" />| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
required | Badge text (also used as the a11y label) |
paddingY |
string |
"0.4em" |
Vertical padding |
paddingX |
string |
"0.8em" |
Horizontal base padding |
balanceRatio |
number |
0.3 |
Extra right padding fraction for optical balance |
borderRadius |
string | number |
"999px" |
Border radius |
gap |
string | number |
"0.35em" |
Gap between glyph and label |
Plus all Spinner props.
Renders a sliding window of recent frames with decreasing opacity — creates a ghost/motion-blur trail effect.
<SpinnerTrail name="helix" color="#7c3aed" size="2rem" trailLength={6} />| Prop | Type | Default | Description |
|---|---|---|---|
trailLength |
number |
4 |
Number of ghost frames (including the current one) |
minOpacity |
number |
0.1 |
Opacity of the oldest ghost frame |
reverse |
boolean |
false |
Flip the opacity gradient (bright on left, fading right) |
Plus all Spinner props.
A <button> with a built-in loading state. Disables itself and shows a spinner when loading is true. Fully forwarded ref.
<SpinnerButton
loading={isSaving}
onClick={handleSave}
spinnerProps={{ name: 'braille', color: '#00ff99' }}
>
Save changes
</SpinnerButton>| Prop | Type | Default | Description |
|---|---|---|---|
loading |
boolean |
false |
Show spinner and disable the button |
spinnerPosition |
"left" | "right" |
"left" |
Which side the spinner appears on |
spinnerGap |
string | number |
"0.45em" |
Gap between spinner and button label |
spinnerProps |
Omit<BaseSpinnerProps, "size"> |
— | Props forwarded to the inner <Spinner> |
All standard <button> attributes are forwarded.
Wraps any content and renders a centered spinner overlay when active. Uses the native inert attribute to block keyboard/pointer access to the content beneath.
<SpinnerOverlay
active={isLoading}
name="orbit"
color="#7c3aed"
size="2.5rem"
backdrop="rgba(0,0,0,0.4)"
>
<YourContent />
</SpinnerOverlay>| Prop | Type | Default | Description |
|---|---|---|---|
active |
boolean |
true |
Show the overlay |
backdrop |
string |
"rgba(0,0,0,0.35)" |
CSS background for the backdrop |
size |
string | number |
"2rem" |
Spinner glyph size |
containerStyle |
CSSProperties |
— | Style applied to the outer wrapper <div> |
containerClassName |
string |
— | Class applied to the outer wrapper <div> |
children |
ReactNode |
— | Content that gets overlaid |
Plus all Spinner props.
Sets global defaults for every Spinner-family component in the subtree. Individual props override context values.
<SpinnerProvider
defaultName="orbit"
defaultColor="#7c3aed"
defaultSpeed={1.2}
respectReducedMotion={true}
>
<App />
</SpinnerProvider>
// Inside: no props needed — picks up context defaults
<Spinner />
<SpinnerBadge label="Live" />| Prop | Type | Default | Description |
|---|---|---|---|
defaultName |
SpinnerName |
"braille" |
Default spinner name |
defaultColor |
string |
undefined |
Default color |
defaultSize |
string | number |
undefined |
Default glyph size |
defaultSpeed |
number |
1 |
Default speed multiplier |
defaultShape |
"circle" | "square" | "diamond" |
undefined |
Default dot shape |
respectReducedMotion |
boolean |
true |
Pause animations when the OS has prefers-reduced-motion: reduce |
Drive any element with the raw frame string for fully custom rendering.
import { useSpinner } from 'cli-loaders';
function MyComponent() {
const frame = useSpinner('helix', 1.5);
return (
<h1 style={{ fontFamily: 'monospace', color: '#00ff99' }}>
{frame}
</h1>
);
}useSpinner(
name: SpinnerName,
speed?: number, // default 1
paused?: boolean, // default false
ignoreReducedMotion?: boolean // default false
): stringReturns a sliding window of the last length frames — the primitive behind SpinnerTrail.
useSpinnerFrames(
name: SpinnerName,
length?: number, // default 3
speed?: number,
paused?: boolean,
ignoreReducedMotion?: boolean
): string[]All 19 names are available as the SpinnerName union type.
import { spinnerNames } from 'cli-loaders';
// ['braille', 'braillewave', 'dna', ...]- The animated glyph is wrapped in
aria-hidden="true"— it is invisible to screen readers. - A visually-hidden
<span role="status" aria-live="polite">announces thelabelprop (default:"Loading"). SpinnerButtonsetsaria-busyandaria-disabledon the button element automatically.SpinnerOverlaysetsaria-busyon the container and uses the nativeinertattribute to prevent keyboard/AT access to obscured content.- By default, all animations respect
prefers-reduced-motion: reduce. Override per-component withignoreReducedMotionor globally via<SpinnerProvider respectReducedMotion={false}>.
Can I use this with Next.js? Yes. Works as both client and server components in the App Router.
How do I customize spinner animations?
Use the useSpinner hook to drive any custom rendering. See useSpinner hook for examples.
Does this work with TypeScript?
Full type support. All component props and the DotShape type are exported for type safety.
How do I change shapes (circles, squares, diamonds)?
Use the shape prop: <Spinner shape="square" />. See Spinner props for details.
Does this support reduced motion preferences?
Yes, by default. Animations pause when prefers-reduced-motion: reduce is set. Override with ignoreReducedMotion={true}.
What's the bundle size? ~15KB gzipped for the core library. SVG rendering mode adds ~1KB.
const [loading, setLoading] = useState(false);
async function handleSubmit() {
setLoading(true);
try {
await api.post('/submit', data);
} finally {
setLoading(false);
}
}
return (
<SpinnerButton
loading={loading}
onClick={handleSubmit}
spinnerProps={{ name: 'helix', color: '#00ff99' }}
>
{loading ? 'Submitting...' : 'Submit'}
</SpinnerButton>
);const [fetching, setFetching] = useState(false);
return (
<SpinnerOverlay
active={fetching}
name="orbit"
size="2rem"
backdrop="rgba(0,0,0,0.5)"
>
<Dashboard data={data} />
</SpinnerOverlay>
);return (
<SpinnerInline
name="scan"
shape="diamond"
color="#7c3aed"
>
Syncing your data...
</SpinnerInline>
);Bug reports and pull requests are welcome. Please open an issue before submitting large changes.
# Install dependencies
npm install
# Start the interactive demo
npm run dev
# Build the library
npm run build