Unicode character spinners for React, driven by requestAnimationFrame.
bun add guilty-spark
# or: npm i guilty-spark / pnpm add guilty-sparkReact 18+ is a peer dependency.
import { Spinner, Loading, PRESETS } from 'guilty-spark'
// drop-in component
<Spinner {...PRESETS.BRAILLE_WAVE_3} />
// with label
<Loading {...PRESETS.DOTS_CYCLE} side="right">thinking</Loading>
// hook — access raw string or char array
const { raw, chars } = useSpinner(PRESETS.BLOCKS_WAVE_3)<Spinner
glyphs={GLYPHS.BLOCKS} // required
generator={sineWave(600)} // required
width={5} // slots (default: 1)
interval={80} // ms per tick (overrides generator default)
label="loading" // aria-label (default: "loading")
className="..."
style={...}
/>Wraps <Spinner> with a text label. side controls which side the spinner appears on.
<Loading {...PRESETS.BRAILLE_WAVE_3} side="right">thinking</Loading>
<Loading {...PRESETS.DOTS_CYCLE} side="left">loading</Loading>Props: all SpinnerDef fields + children, side, className, style, spinnerClassName, spinnerStyle.
import { PRESETS } from 'guilty-spark'| Key | Glyphs | Generator | Width |
|---|---|---|---|
BRAILLE_WAVE_3 |
BRAILLE | sineWave | 3 |
BRAILLE_DENSITY |
BRAILLE | densityCurve | 1 |
DOTS_CYCLE |
DOTS | staticFrames | 1 |
DOTS_BOUNCE |
DOTS | pingPong | 1 |
BLOCKS_WAVE_3 |
BLOCKS | sineWave | 3 |
BLOCKS_WAVE_5 |
BLOCKS | sineWave | 5 |
BLOCKS_FILL |
BLOCKS | pingPong | 1 |
STARS_MORPH |
STARS | densityCurve | 1 |
CIRCLE_SPIN |
CIRCLE_QUAD | staticFrames | 1 |
DICE_BOUNCE |
DICE | pingPong | 1 |
ARROWS_SPIN |
ARROWS_8 | staticFrames | 1 |
TRIANGLE_SPIN |
TRIANGLES | staticFrames | 1 |
Presets are plain SpinnerDef objects — spread and override any field:
<Spinner {...PRESETS.BRAILLE_WAVE_3} width={7} interval={40} />import { GLYPHS } from 'guilty-spark'
GLYPHS.BLOCKS // [' ','▁','▂','▃','▄','▅','▆','▇','█']
GLYPHS.BRAILLE // [' ','⠁','⠃','⠇','⠏','⠟','⠿','⣿']
GLYPHS.DOTS // ['⁚','⁙','⁘','⁛','⁕','⁜','⁝','⁞']
GLYPHS.STARS // ['⁎','⁑','⁂','※']
GLYPHS.CIRCLE_QUAD // ['◴','◵','◶','◷']
GLYPHS.DICE // ['⚀','⚁','⚂','⚃','⚄','⚅']
GLYPHS.ARROWS_4 // ['←','↑','→','↓']
GLYPHS.ARROWS_8 // ['←','↖','↑','↗','→','↘','↓','↙']
GLYPHS.TRIANGLES // ['▲','▶','▼','◀']Any readonly string[] works as a custom glyph set.
| Factory | Description | Default interval |
|---|---|---|
staticFrames(frames?) |
Steps through a frames array; slots show a sliding window | 80 ms |
densityCurve(density, steps?) |
Interpolates a [0,1] curve across the glyph set |
80 ms |
sineWave(period?) |
Continuous sine wave per slot, phase-offset for multi-slot travel | every rAF frame |
pingPong(['a','b','c','d']) // => ['a','b','c','d','c','b'] (bounce sequence)
loop(['a','b','c']) // => ['a','b','c'] (forward cycle, explicit copy)
defineSpinner({ glyphs: GLYPHS.BRAILLE, generator: sineWave(800), width: 4 })import { Spinner, defineSpinner, GLYPHS, sineWave, staticFrames, loop } from 'guilty-spark'
const MY_SPINNER = defineSpinner({
glyphs: GLYPHS.BRAILLE,
generator: staticFrames(loop([...GLYPHS.BRAILLE])),
width: 1,
})
<Spinner {...MY_SPINNER} />Custom generators implement GlyphGenerator:
import type { GlyphGenerator } from 'guilty-spark'
const random: GlyphGenerator = {
initialState: 0,
recommendedInterval: 120,
nextState: (s) => s + 1,
frame(_s, _i, _w, glyphs) {
return glyphs[Math.floor(Math.random() * glyphs.length)]
},
}MIT
