Contour loaderPro
Topographic contour lines over slowly evolving simplex noise: a calm alternative to a spinner for large panels, an empty canvas or a workspace that is indexing.
On this page
ProA workspace panel while the agent indexes a repository: contour lines drift behind the label and a live file count.
Installation
Usage
import { ContourLoader } from "@/components/ui/chatcn/pro/contour-loader"
<ContourLoader label="Indexing the repository" className="aspect-video">
<span className="tabular-nums">{indexed} of {total} files</span>
</ContourLoader>The loader fills its box, so give it a size. Lines are traced with marching squares on a 96 column grid (rows follow the aspect ratio, so cells stay square) and drawn as 1 px strokes in two gray tokens, every fourth line one step darker as an index contour. The third noise axis moves with time, so the terrain evolves in place instead of sliding. Lines fade out around the label so text never sits on a line.
Examples
Variants
Dense and sparse maps, a paused loader with a label, a fine slow grid, stronger lines and right to left.
ProDense and sparse maps, a fine grid, stronger lines and a paused loader.
Props
| Prop | Type | Notes |
|---|---|---|
label | ReactNode | What is happening, in the middle with the shimmer |
ariaLabel | string | Accessible name when label is not text. Default "Loading" |
children | ReactNode | Extra content under the label |
levels | number | Iso lines. Default 9 |
indexEvery | number | Every nth line is darker. 0 turns it off. Default 4 |
grid | number | Marching squares columns. Default 96 |
scale | number | Features across the long side. Default 2.4 |
speed, fps | number | Default 1 and 24 (capped at 30) |
seed | number | Seeds the terrain, so two loaders differ. Default 3 |
still | number | Seconds into the loop for the reduced motion frame. Default 8 |
stroke, indexStroke | string | Default gray-600 and gray-800 tokens |
clearLabel | boolean | Fade lines around the label. Default true with a label |
delay | number | Wait before anything shows. Default 300 ms |
paused, defaultPaused, onPausedChange | Controlled or uncontrolled pause | |
pauseControl | boolean | Show the pause button. Default true |
Keyboard
| Key | Action |
|---|---|
Tab | Reaches the pause button |
Enter / Space | Pauses or plays the lines |
Accessibility
- The root is
role=statuswith your label as its accessible name. The pattern isaria-hidden. - Content you pass as
children(a live file count, for example) is not announced on every tick. Announce the end of the wait yourself, where the result appears. - The pause button has a name such as
Pause contour lines, a 32 px hit area and 44 px on touch screens. - Under
forced-colors: activethe pattern hides and only the label shows, in system colors.
Motion
- Nothing shows for the first 300 ms, so a fast load shows no loader at all. Once it is visible it stays at least 400 ms, so it never flashes.
- The loop stops offscreen and in a background tab, and the pause button in the corner stops it for good. Paused time does not count, so play continues where it left off.
- Under reduced motion one still frame holds and the label stops shimmering. Nothing moves.
- The pattern fades in over 300 ms when it appears.