Zeno Progress

Loading Progress Bar for React

A progress bar that closes half the remaining gap at every step, so it never reaches the end while work is still running. The last stretch becomes a ruler that keeps zooming into itself, and the gap snaps shut the moment the real result arrives.

  • Progress
  • Loading
  • AI
  • Canvas

Uses motion, clsx, tailwind-merge

Inspired by Jujutsu Kaisen

Open in ChatGPTOpen in Claude
interval1000ms
snapDuration0.36s
rulertrue
<ZenoProgress />

The demo follows as you drag.

Your image appears here.

Prompt

Alpine ridge at first light, low cloud pooled in the valley, 35mm film

Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Zeno Progress: A progress bar that closes half the remaining gap at every step, so it never reaches the end while work is still running. The last stretch becomes a ruler that keeps zooming into itself, and the gap snaps shut the moment the real result arrives.
This demo, Image Generation: A prompt card. Generate runs Composing, Denoising and Upscaling while the model takes a random 4 to 9 seconds, so the bar is often deep in the ruler when the image arrives; the gap snaps shut and the image resolves in. Generate again for another take.

1. Install it:
   npx shadcn@latest add @keyframeui/zeno-progress-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/zeno-progress-demo.json instead.)
   It adds the demo, components/zeno-progress-demo.tsx, and the component, components/ui/zeno-progress.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <ZenoProgress> on its own.

3. These props set its feel. Change them before you change the file:
   - interval (default 1000): Milliseconds for each halving of the gap. The fill passes 90% after about 3.7 intervals and gains a decimal every 3.3 after that.
   - snapDuration (default 0.36): Seconds the gap takes to snap shut once `done` arrives, on a spring that never overshoots. The ticks flash as it lands.
   - ruler (default true): Past 90%, turn the end of the track into a magnifier: the gap is re-scaled to fill it, ticks stream out of the end point and the percentage gains decimals. Off, the bar just crawls.

4. Don't change, in components/ui/zeno-progress.tsx:
   - One rAF loop draws the bar on a canvas and writes the percentage and `aria-valuenow` straight to the DOM, so a run never re-renders except when the step label changes.
   - The gap is tracked as log10 and only ever halves, so the fill never reaches the end on its own; only `done` closes it, on a spring with `bounce: 0` that never overshoots.
   - The zoom window freezes when `done` arrives, so the snap runs through the magnified gap.
   - The percentage is floored and never reads 100% until the fill has landed.
   - Reduced motion drops the ruler and zoom: past 90% the track breathes instead, `done` jumps the fill and labels only fade.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/zeno-progress.md

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/zeno-progress.tsx and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/zeno-progress

Examples

Finished sections built with Zeno Progress. Each one installs whole, with the component included.

Deploy Log

A deploy streams its build log under a bar in the header, labelled with the current phase. Waiting for the health check hangs for seconds with nothing new in the log, and the ruler zooms the whole time until the 200 lands.

storefrontproduction
Queued
Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Zeno Progress: A progress bar that closes half the remaining gap at every step, so it never reaches the end while work is still running. The last stretch becomes a ruler that keeps zooming into itself, and the gap snaps shut the moment the real result arrives.
This demo, Deploy Log: A deploy streams its build log under a bar in the header, labelled with the current phase. Waiting for the health check hangs for seconds with nothing new in the log, and the ruler zooms the whole time until the 200 lands.

1. Install it:
   npx shadcn@latest add @keyframeui/zeno-progress-deploy-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/zeno-progress-deploy-demo.json instead.)
   It adds the demo, components/zeno-progress-deploy-demo.tsx, and the component, components/ui/zeno-progress.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <ZenoProgress> on its own.

3. These props set its feel. Change them before you change the file:
   - interval (default 1000): Milliseconds for each halving of the gap. The fill passes 90% after about 3.7 intervals and gains a decimal every 3.3 after that.
   - snapDuration (default 0.36): Seconds the gap takes to snap shut once `done` arrives, on a spring that never overshoots. The ticks flash as it lands.
   - ruler (default true): Past 90%, turn the end of the track into a magnifier: the gap is re-scaled to fill it, ticks stream out of the end point and the percentage gains decimals. Off, the bar just crawls.

4. Don't change, in components/ui/zeno-progress.tsx:
   - One rAF loop draws the bar on a canvas and writes the percentage and `aria-valuenow` straight to the DOM, so a run never re-renders except when the step label changes.
   - The gap is tracked as log10 and only ever halves, so the fill never reaches the end on its own; only `done` closes it, on a spring with `bounce: 0` that never overshoots.
   - The zoom window freezes when `done` arrives, so the snap runs through the magnified gap.
   - The percentage is floored and never reads 100% until the fill has landed.
   - Reduced motion drops the ruler and zoom: past 90% the track breathes instead, `done` jumps the fill and labels only fade.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/zeno-progress.md

Upload List

Three files, each with its own bar, finishing at different times. The video's virus scan takes much longer, so you can watch its ruler go many decimals deep while the others are done.

Uploads

0 of 3 ready

  • brand-guidelines.pdf

    4.2 MB

    Uploading
  • hero-loop.mp4

    86.1 MB

    Uploading
  • Q3-review.key

    18.9 MB

    Uploading
Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Zeno Progress: A progress bar that closes half the remaining gap at every step, so it never reaches the end while work is still running. The last stretch becomes a ruler that keeps zooming into itself, and the gap snaps shut the moment the real result arrives.
This demo, Upload List: Three files, each with its own bar, finishing at different times. The video's virus scan takes much longer, so you can watch its ruler go many decimals deep while the others are done.

1. Install it:
   npx shadcn@latest add @keyframeui/zeno-progress-upload-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/zeno-progress-upload-demo.json instead.)
   It adds the demo, components/zeno-progress-upload-demo.tsx, and the component, components/ui/zeno-progress.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <ZenoProgress> on its own.

3. These props set its feel. Change them before you change the file:
   - interval (default 1000): Milliseconds for each halving of the gap. The fill passes 90% after about 3.7 intervals and gains a decimal every 3.3 after that.
   - snapDuration (default 0.36): Seconds the gap takes to snap shut once `done` arrives, on a spring that never overshoots. The ticks flash as it lands.
   - ruler (default true): Past 90%, turn the end of the track into a magnifier: the gap is re-scaled to fill it, ticks stream out of the end point and the percentage gains decimals. Off, the bar just crawls.

4. Don't change, in components/ui/zeno-progress.tsx:
   - One rAF loop draws the bar on a canvas and writes the percentage and `aria-valuenow` straight to the DOM, so a run never re-renders except when the step label changes.
   - The gap is tracked as log10 and only ever halves, so the fill never reaches the end on its own; only `done` closes it, on a spring with `bounce: 0` that never overshoots.
   - The zoom window freezes when `done` arrives, so the snap runs through the magnified gap.
   - The percentage is floored and never reads 100% until the fill has landed.
   - Reduced motion drops the ruler and zoom: past 90% the track breathes instead, `done` jumps the fill and labels only fade.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/zeno-progress.md

Props

done

TypebooleanDefaultfalse

The work is finished. The bar only reaches the end when this turns true, from wherever it is: the gap snaps shut on a spring that never overshoots. Turning it false again rewinds the bar and starts a new run.

steps

Typestring[]

Labels that take turns on the left as the fill moves, sliding up into place. They're spread over the run up to 90%, so the last one holds through the crawl.

label

TypestringDefault"Working"

A fixed label while running, for when your own code knows the phase. Overrides steps.

doneLabel

TypestringDefault"Done"

Label once done is true. It's also announced to screen readers.

interval

TypenumberDefault1000

Milliseconds for each halving of the gap. The fill passes 90% after about 3.7 intervals and gains a decimal every 3.3 after that.

snapDuration

TypenumberDefault0.36

Seconds the gap takes to snap shut once done arrives, on a spring that never overshoots. The ticks flash as it lands.

ruler

TypebooleanDefaulttrue

Past 90%, turn the end of the track into a magnifier: the gap is re-scaled to fill it, ticks stream out of the end point and the percentage gains decimals. Off, the bar just crawls.

onComplete

Type() => void

Called once the fill reaches the end after done, as the ticks flash. Use it to reveal the result.

barClassName

TypestringDefault"text-zinc-900 dark:text-zinc-100"

Classes that set the bar's color (a text-* color). The track and the ruler's ticks are drawn from it.

className

Typestring

Classes for the root. Its text size and color style the labels.