Loader
Nine loading animations behind one variant prop.
A loading animation, in nine flavours. They are interchangeable — pick one for the character it gives a screen, then use the same one everywhere — so the thing that varies is a value rather than a component. Every variant takes the same colour, the same tempo and the same treatment under reduced motion, which means swapping one for another is a one-word change.
All of it runs on the UI thread, and none of it re-renders anything while it runs.
Installation
Loader ships with the library — no separate install.
import { Loader, Button, Card, Text } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add loaderUsage
<Loader variant="wave-physics" />Examples
Choosing a variant
Nine of them. The dot and bar variants are the quiet ones, for a loader sitting inside something else; liquid-dots, morph-ring and wave-physics have more character, and earn their place on a screen that is otherwise empty.
<Loader variant="pulse-dots" /> {/* the default */}
<Loader variant="bounce-dots" />
<Loader variant="pulsating-dots" />
<Loader variant="liquid-dots" />
<Loader variant="bar-cascade" />
<Loader variant="bouncing-bars" />
<Loader variant="symmetric-wave" />
<Loader variant="morph-ring" />
<Loader variant="wave-physics" />Colour
color takes a theme token by name as readily as a literal, and the token is nearly always what you want — a literal is a colour frozen at the moment it was written, and it cannot follow the theme into dark mode.
<Loader variant="bar-cascade" color="--color-info" />
<Loader variant="symmetric-wave" color="--color-success" />
{/* A literal, for a colour that is not in the theme. */}
<Loader variant="morph-ring" color="#2dd4bf" />Inside a button
With no colour of its own the loader draws in the readable foreground of the surface it is on. That matters most exactly here: a filled button is usually painted in the primary colour, and in most themes the primary colour is the page’s foreground — so a loader that resolved the page’s ink for itself would be invisible on the one surface it appears on most.
<Button disabled={saving} onPress={save}>
{saving ? <Loader variant="pulse-dots" size="sm" /> : 'Save changes'}
</Button>Size and tempo
size scales the whole drawing rather than cropping it, so the proportions hold. speed multiplies the tempo — below 1 for a loader that should feel unhurried, above it for one covering something that is nearly done.
<Loader size="sm" />
<Loader size="md" /> {/* the default */}
<Loader size="lg" />
<Loader variant="wave-physics" speed={0.6} />
<Loader variant="wave-physics" speed={1.8} />What a screen reader hears
The loader announces itself as a progress indicator, labelled “Loading” by default. Say what is loading when the loader is the only thing on the screen — “Loading” on its own tells someone nothing they did not already know from the wait.
<Loader variant="wave-physics" label="Loading your reports" />Versions
The nine
All of them side by side at the same size and colour, which is the only way to actually choose between them.
{VARIANTS.map((variant) => (
<Card key={variant}>
<Card.Content className="flex-row items-center gap-4 py-5">
<View className="w-32 items-center">
<Loader variant={variant} />
</View>
<Text size="sm" muted className="flex-1">{label(variant)}</Text>
</Card.Content>
</Card>
))}Waiting for a screen
Where a loader usually goes: standing in for content that has not arrived, and gone the moment it has.
{loading ? (
<Loader variant={variant} size="lg" label="Loading your reports" />
) : (
<Text size="lg" weight="semibold">12 reports</Text>
)}In place
Inside buttons and cards, taking its ink from the surface it landed on rather than from the page.
<Button disabled={busy} onPress={save}>
{busy ? <Loader variant="pulse-dots" size="sm" /> : 'Save changes'}
</Button>API Reference
Loader
| Prop | Type | Default | Description |
|---|---|---|---|
variant | LoaderVariant | 'pulse-dots' | Which animation to draw. They are interchangeable — pick one for the character it gives the screen and keep it. |
size | LoaderSize | 'md' | Overall scale. The geometry of every variant is multiplied by it. |
color | string | — | Ink. Takes a theme token by name — "--color-primary" — as readily as a literal, and the token is usually what you want: a literal cannot follow the theme into dark mode. With neither, the loader draws in the readable foreground of the surface it is on, so one inside a filled button is legible without being told. |
speed | number | 1 | Multiplier on the tempo. 1 is the designed speed; 2 is twice as fast. |
label | string | 'Loading' | What a screen reader announces. Defaults to "Loading". Say what is loading when the loader is the only thing on the screen. |
className | string | — |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
Choosing one
The three dot variants and the three bar variants are quiet enough to sit inside something else — a button, a row, a card header. liquid-dots, morph-ring and wave-physics have more character and want a screen that is otherwise empty; used in a button they draw attention to the wait rather than away from it.
Whichever you pick, pick one. A loader is part of an app’s voice, and an app that uses a different one on every screen reads as several apps.
How they are drawn
Anything with a small, fixed number of moving pieces is a view per piece with its own animated style: three dots is three transforms a frame, which is cheaper than rebuilding a path string.
Anything whose count is high, or whose shape is derived rather than declared, is a single SVG path rebuilt on the UI thread. wave-physics is fifteen bars responding to a bouncing ball — one animated prop drawn that way, fifteen drawn the other, and the fifteen would not stay in step with each other for free.
liquid-dots is the odd one. The merge where the two blobs meet is normally a blur-and-threshold filter pass, and SVG filters do not render on native at all — so the two circles and the neck between them are three subpaths of one path, unioned by the non-zero fill rule. It costs one animated prop a frame and gives a crisper edge than a threshold does.
Reduced motion
Every variant draws a representative still frame rather than freezing wherever it happened to be. A stopped animation should look like a shape someone drew, not like a bug caught mid-cycle — so the dots hold an uneven set of opacities, the bars hold the shape of their wave, and the ball rests at the top of a hop.
speed={0} does the same thing deliberately, for a loader that should be present but still.