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 loader

Usage

<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

PropTypeDefaultDescription
variantLoaderVariant'pulse-dots'Which animation to draw. They are interchangeable — pick one for the character it gives the screen and keep it.
sizeLoaderSize'md'Overall scale. The geometry of every variant is multiplied by it.
colorstringInk. 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.
speednumber1Multiplier on the tempo. 1 is the designed speed; 2 is twice as fast.
labelstring'Loading'What a screen reader announces. Defaults to "Loading". Say what is loading when the loader is the only thing on the screen.
classNamestring

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.

On this page