Loaders
Loaders are loading animations with depth, drawn in your theme. Particle fields in perspective, wireframes and lit solids, waveforms, the youj letterforms and moments from the tools people wait in, each a polite status and a still frame under reduced motion.
When something takes a moment (a workspace being prepared, a model thinking, a build, an upload), show it at work. Each loader is a small scene in perspective, drawn live in your text color with one mark in your accent and a few touches of your theme's colors, on the motion tokens' tempo. It's a status that announces its label politely, and under reduced motion it holds one still frame that still reads as busy.
The loaders gallery shows every one of them live: filter them by category, set the size, the length of a loop, the colors and the labels, then copy the code or the install command.
The set
| Category | What it draws |
|---|---|
| Color | Built around your theme's colors: hue travelling through a structure, layered tints, color as state |
| Depth | Particle fields in perspective, nearer marks larger and brighter |
| Geometry | Wireframes, planes lit by their facets, surfaces drawn in lines |
| Signal | Waveforms and live data, drawn as they arrive |
| Glyph | The youj letterforms, drawing, turning and assembling |
| Interface | Cursors, focus rings, pipelines and construction marks |
Every loader
A loader's id is its entry, @youj/loaders/<id>, and its component is Loader and the id in PascalCase (tittle is LoaderTittle). The ids, by category:
| Category | Loader ids |
|---|---|
| Color | prism overprint spectrum gyre bloom litmus weave swatch beacon chord isotherm |
| Depth | tittle drop layers corridor slice vortex murmur reeve |
| Geometry | nacre draft carton extrude section ridge contour pleat lathe saddle torsion facet core band curl tenon selvedge meniscus atlas arbor banyan tessera vault knead lacuna stave suture ravel throat truss mosaic tributary alveole calyx quarry newel infill atrium shingle carve lamella simplex ashlar laminate loft stayed reciprocal envelope retile cupola coarea corbel notch datum arris cast ogive |
| Orbs | gores lamina vesicle cleave chordal armature brim cincture respire pith |
| Mechanics | mesh dwell yoke impulse iris tackle heddle gimbal lattice siphon detent bellows tread fulcrum auger epicycle bolus nutate |
| Science | ligate valence halite chaperone lobule chamber adduct exchange kekule catenate |
| Waves | fulgur tensor duplex chirp overtone fletch sounding evanesce bragg whisper |
| Signal | cue phosphor sustain isobar tail tempo locus vitals harmonics delta least-squares bell spool wick caustic umbra flux fringe capillary refract azimuth apsis nodal analyzer descent quantile leader overturn barcode blossom |
| Glyph | inscribe jot spell compose kern hint plumb |
| Interface | marquee tabstop build prompt measure register queue gantry sift conflux parity stow fathom refine lexicon raster alcove escrow sachet swizzle medoid schema prefix trellis collate docket stencil coalesce folio occlude accord tensile adjoint isoline radix pivot stride bisect reduce meander entail condense minify traverse splice salvage |
| Software | mullion tangent scion quire graft verdict chorus casement cadence ledger winnow offshoot outpost amanuensis valance pipette portico raccord elide attest satchel |
Installation
From the package
Every loader ships in @youj/loaders, at its own entry, as Loader and its name:
npm i @youj/loadersimport { LoaderTittle } from '@youj/loaders/tittle'One loader, as source
To own and edit one loader, copy it in with the Youj CLI in an app set up from source (npx @youj/cli init --mode registry). Every loader is its own item, loader-<id>. It brings the kit the loaders are drawn with (loader-kit: the status, the shared ticker and the engine that draws in depth) and nothing else. In an app on the packages, the same command installs @youj/loaders instead and names the entry to import:
npx @youj/cli add loader-tittleIt lands in components/youj/loaders/<category>/<id>.tsx, and you import it from there through your @/ alias:
import { LoaderTittle } from '@/components/youj/loaders/depth/tittle'Each loader's id, import and install command are on its card in the gallery.
Usage
import { LoaderSustain } from '@youj/loaders/sustain'
export function Transcript({ busy }: { busy: boolean }) {
if (busy) return <LoaderSustain size="lg" label="Transcribing" showLabel />
// …
}Size
size is the drawing's height: sm (24px, beside text), default (40px, in a control or a card), lg (64px, on a panel) or any number of pixels. The wordmark loaders and most of the signals are wider than they are tall. Each draws for its size: fewer, bolder marks when it's small, finer detail when it's large.
Color
A loader's structure is drawn in its ink, the text color (currentColor) by default, so it takes the color of whatever it sits in; color sets it to any CSS color, token references included. Its accent is on the one mark that matters (lit accent faces included) and in a shown label's light and caret: the theme's accent by default, and accentColor sets it the same way.
That is all a loader uses unless you ask for more. Pass colors="theme" and a few parts take colors derived from your accent: what moves or works (a trail, a path being traced) in one, a second layer in another, and a finished state (a check, an arrival) in your success color. The color is quiet: thin marks and small particles, never more than about a third of the drawing, and it moves with the loop, a part warming toward its color on the beat, new marks arriving in color and settling to the ink, a hue travelling once through a structure. The colors are muted and follow your theme's accent, in light and dark, so the accent stays the brightest thing in the drawing.
<LoaderIsobar color="var(--color-fg-muted)" />
<LoaderTittle color="var(--color-fg-muted)" accentColor="var(--color-success)" />colors is off by default: a loader draws in its ink and its accent alone. colors="theme" turns the colored parts on in the theme's colors. A list sets their colors instead, in order, with any CSS color or token reference; an empty entry, or one you leave out, keeps the theme color. A loader without colored parts ignores it. A loader's parts are named in the gallery, whose Color control switches between Mono (the default) and Theme and, with Theme on, picks each part's color.
<LoaderPrompt colors="theme" />
<LoaderGantry colors={['var(--color-fg-muted)', 'var(--color-success)']} />
<LoaderGantry colors={['', 'var(--color-success)']} />Solid planes and lit faces hide what's behind them by painting on a paper: --yj-anim-paper, the surface by default. On another background, set it to that background, so the faces sit on what's really there:
<div className="bg-canvas [--yj-anim-paper:var(--color-canvas)]">
<LoaderCarton size="lg" />
</div>Pace
A loop lasts a few beats of the deliberate duration, so the theme's personality and the provider's time scale reach it: in a precise theme a beat is 560ms, and a loop takes a few seconds. speed multiplies on top (2 is twice as fast). A change of speed carries on from the frame it's on, and paused holds the frame.
Announcing the wait
The status is named by label, "Loading" by default (from the labels, so it's translated with the rest). When something around it already says what's happening (a button that reads "Saving…"), pass label={null} and the loader is only a picture.
Showing the label
showLabel shows the label with the drawing: centered under it by default, in the theme's label voice, with the space between them (and on a large drawing, the type) growing with size. labelPosition puts it over the drawing (top), or beside it, before (start) or after (end) it in the reading direction, so it changes sides in a right-to-left language. Beside the drawing it takes the text style of the line it sits in. Over or under it, one loader's label can take its own type through the theme's label properties and --yj-label-size: className="[--yj-label-case:uppercase] [--yj-label-font:var(--font-mono)] [--yj-label-size:15px]".
<LoaderBuild size="lg" label="Building" showLabel />
<LoaderTittle size="sm" label="Syncing" showLabel labelPosition="end" />A shown label moves in time with the drawing, on its clock: it holds when the drawing is paused or off screen, and keeps its pace through speed and the time scale. labelEffect picks how:
| Effect | What it does |
|---|---|
pulse (default) | A soft light, the ink with a touch of the accent, sweeps through the muted label once a loop, then rests. |
wave | A gentle lift travels through the letters, first to last, once a loop, then rests. |
type | The label types itself a letter at a time behind a square-cornered caret in the accent, holds while the caret blinks, and is cleared by the caret gliding home, so each loop starts on an empty line. |
scramble | The line fills with glyph noise and the letters resolve out of it one at a time, first to last; the word holds, then breaks back into noise that clears before the loop ends. |
slide | At the loop's seam the line rolls up, like a counter: the label rises out as the next rises in. It's made for several labels; one label rolls over to itself. |
none | The label holds still. |
<LoaderPrompt size="lg" label="Thinking" showLabel labelEffect="type" />The drawn label is only a picture of the words: assistive technology hears the plain label once, never letters arriving. It's static in the server's HTML, and starts moving at a moment of the loop that looks just like it, so nothing jumps when the script arrives. In a right-to-left language the effects run from the right. Letters move one by one (wave, and scramble's noise) only in scripts whose letters stand on their own; a joined script such as Arabic keeps its letters whole, wave passing a light through the word and scramble resolving it as a plain reveal.
Rotating labels
A wait that takes a while can say more than one thing. Give label a list, and a shown label turns through it: each one shows for labelEvery loops (one by default), and the next takes over at the loop's seam, the effect carrying the change: the light dips, the letters lift away and rise in, the caret clears one and types the next, the noise resolves into it, or the line rolls up. Then it starts again from the first.
<LoaderBuild
size="lg"
label={['Building', 'Bundling', 'Optimizing images', 'Deploying']}
showLabel
labelEffect="slide"
/>
<LoaderTittle
size="lg"
label={['Loading', 'Generating results', 'Almost there']}
labelEvery={2}
showLabel
labelEffect="wave"
/>The labels sit in one place as wide as the longest, so nothing around them moves as they change. The status doesn't read each one out: it says the first, then a later one only as it shows, in order, once each and at least ten seconds after the last, and then it stays quiet, however long the wait. When the state really changes (an upload finishes and processing starts), pass a new label: that's said at once. Without showLabel, the labels still take turns for assistive technology the same way. Under reduced motion the first label shows, still, and stays.
Performance
Each loader draws on one small canvas, on a ticker every loader on the page shares, and only while it's on screen in a visible tab: offscreen, hidden or paused, it costs nothing. Its colors are read once, and again when the theme changes. The server sends its still frame as SVG, so it's never blank before the script runs.
Reference
Propssize, color, accentColor, colors… (12)
Props
| Prop | Type | |
|---|---|---|
size | LoaderSizeThe drawing's height: default `default` | `default` |
color | stringThe primary color: its ink. Any CSS color, token references included ( | – |
accentColor | stringThe secondary color: the accent, on the one mark that matters (lit accent faces included) and in a shown label's light and caret. Any CSS color or token reference. Default: the theme's accent. | – |
colors | readonly string[] | 'theme'Turns color on for the loader's colored parts (its extra tones; see its docs for which is which). | – |
speed | numberPlayback rate over the theme's tempo: 2 is twice as fast, 0.5 half. It multiplies with the provider's time scale. default 1 | 1 |
label | string | readonly string[]The accessible name, announced politely: one label, or several that take turns (a shown label rotates through them, one every | – |
labelEvery | numberWith several labels: how many loops each one shows for before the next takes over, at the seam. default 1 | 1 |
showLabel | booleanShows the label with the drawing, not only to assistive technology. default false | false |
labelPosition | LoaderLabelPosition'bottom''top''start''end'Where a shown label sits: centred under the drawing ( default `bottom` | `bottom` |
labelEffect | LoaderLabelEffect'pulse''wave''type''scramble''slide''none'How a shown label moves, in time with the loop, and changes to the next: default `pulse` | `pulse` |
paused | booleanHolds the current frame. default false | false |
fps | numberThe most frames a second it draws: a cap for ambient use (a loader that plays for a long time beside others), where 30 looks the same at a fraction of the cost. Time still runs at the drawing's tempo. Default: every frame the screen shows. | – |
Other props pass through: extends HTMLAttributes<HTMLSpanElement>. | ||
MotionLoop, moves, arrivals… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Loopthe whole scene, seamless: its last frame is its first | duration.deliberate560ms · × 2.25 to 6.5 beats per loop | 560ms× 2.25 to 6.5 beats per loop | |
| Movestravel with intent: a cursor, a fold, a plate coming into register | ease.emphasizedcubic-bezier(0.6, 0, 0.1, 1) | cubic-bezier(0.6, 0, 0.1, 1) | |
| Arrivalswhat lands or settles: a tittle, a packet, a closing lid | ease.entercubic-bezier(0.16, 1, 0.3, 1) | cubic-bezier(0.16, 1, 0.3, 1) | |
| Departureswhat lets go or leaves the frame | ease.exitcubic-bezier(0.4, 0, 1, 1) | cubic-bezier(0.4, 0, 1, 1) | |
| Label pulsea soft light crosses the shown label once a loop, then rests | ease.emphasizedcubic-bezier(0.6, 0, 0.1, 1) | cubic-bezier(0.6, 0, 0.1, 1) | |
| Label wavea lift of a fifth of an em travels through the letters, first to last, no overshoot | no token a raised cosine per letter | a raised cosine per letter | |
| Label typea letter at a time at an even cadence, then the caret glides home, taking the word before the seam | ease.emphasizedcubic-bezier(0.6, 0, 0.1, 1) | cubic-bezier(0.6, 0, 0.1, 1) | |
| Label scramblenoise fills the line, the letters lock in one at a time at an even cadence, then break back into noise | ease.linearcubic-bezier(0, 0, 1, 1) | cubic-bezier(0, 0, 1, 1) | |
| Label slideat the seam the line rolls up through a soft-edged window the height of the line | ease.emphasizedcubic-bezier(0.6, 0, 0.1, 1) | cubic-bezier(0.6, 0, 0.1, 1) | |
| Next labelat the seam, every labelEvery loops; the effect carries the change | duration.deliberate560ms · × the loop | 560ms× the loop |
Reduced motion
One still frame, chosen to read as busy (mid-move, its depth showing), drawn as SVG. Nothing moves, a shown label holds still (the first of several, which don't turn), and the label is still announced.
Reduced motion
It holds one still frame that still reads as busy: mid-move, with its depth showing. It's the same SVG the server sends, so nothing moves and nothing flashes when the script arrives. A shown label stays whole and still: no light, no lift, no caret, no noise. Of several labels, the first shows and stays: a caption that kept changing on its own would be just the distraction reduced motion asks to be spared, and the first label already says what's happening.
AccessibilityARIA, announcements
Accessibility
- Each loader is
role="status"witharia-live="polite", named by its label: assistive technology hears "Loading" (or your label) once, not the drawing, which isaria-hidden. - A shown label is drawn
aria-hiddentoo: the status keeps the plain label as its name and what it announces, so a typed or scrambled label is never read out letter by letter. - Several labels don't make a talkative status: it says the first, then a later one only as it shows, in order, once each and at least ten seconds apart, then stays quiet. A new
labelis said at once. label={null}makes it decorative (aria-hidden, no status), for when the thing around it already announces the wait.- The accent and the theme's colors mark parts of the drawing, never the only difference: the drawing reads the same without
colorsand in forced colors. - It shows that something is happening, not how far along it is: when you know, show a Progress.