Skip to content

Loader

Proposed

A 3 by 3 pixel grid for work that takes longer than a spinner should: routing a batch, re-scoring a book. Label pulses, the timer counts in mono.

@varick/ui/components/loaderAdded 2 Sept 2026

Routing submissions0.0s

Usage

import { Loader } from "@varick/ui/components/loader";

export function Example() {
  return <Loader label="Routing submissions" />;
}

Variants

Patterns

Drive
Dots
Orbit
<Loader variant="drive" label="Routing submissions" />
<Loader variant="dots" label="Re-scoring the book" />
<Loader variant="orbit" label="Waiting on HubSpot" />

Grid alone, inside other components

Syncing 12 connectors
<LoaderGrid /> Syncing 12 connectors

API reference

Every component also accepts className and the native props of its root element.

PropTypeDefaultDescription
labelstring'Routing submissions'What is happening, as a present participle.
variant'drive' | 'dots' | 'orbit''drive'Drive and Dots run a chevron wavefront; Orbit laps the perimeter.
timerbooleantrueThe elapsed counter, in tenths of a second, then minutes.

Accessibility

  • role="status": the label is announced once, the timer is not re-announced every tick.
  • Reduced motion freezes the grid to its dim state and stops the label pulse; the timer still counts.

Styles

Proposed CSS in Varick tokens. Nothing in the app renders this yet.

.loader-grid { display: grid; grid-template-columns: repeat(3, 4px); gap: 1.5px; }
.loader-grid span { width: 4px; height: 4px; background: var(--fg-high); border-radius: 4px; opacity: 0.15; animation: pixel-on 650ms ease-in-out var(--delay) infinite; }
@keyframes pixel-on { 0%, 100% { opacity: 0.15; } 50% { opacity: 1; } }
.pulse-label { font-size: 13px; font-weight: 500; animation: label-pulse 1.4s ease-in-out infinite; }
@keyframes label-pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }