GodUIGodUI
129Follow on X

Anatomy of the GodUI Progress

Why the fill is always full width, how a new value glides there on a spring, and how the indeterminate bar sweeps without touching layout.

01/03The bar never resizes
tsx
<ProgressPrimitive.Indicatordata-slot="progress-indicator"className="h-full w-full flex-1 bg-primary  transition-[transform] duration-(--godui-duration-slow) ease-spring-smooth  motion-reduce:transition-none …"style={{ transform: `translateX(-${100 - (value || 0)}%)` }}/>
01

The bar never resizes

The obvious way to draw progress is a bar whose width is the percentage. Animate that and the browser runs layout on every frame of the tween.

shadcn already avoids it: the indicator is always the full width of the track and slides left by whatever is left to do, translateX(-(100 - value)%). The track is overflow-hidden, so the part that's slid out is clipped. At 66% the bar sits 34% to the left.

shadcn then transitions it with transition-all, which also tweens any color or size that happens to change. GodUI names transform only and moves it on ease-spring-smooth over 380ms, so each new value lands with a soft settle instead of a linear crawl.

02/03Indeterminate
tsx
"… data-[state=indeterminate]:w-2/5 data-[state=indeterminate]:animate-godui-progress-indeterminate …"@keyframes godui-progress-indeterminate {from { transform: translateX(-100%); }to { transform: translateX(250%); }}/* 1.4s, cubic-bezier(0.65, 0, 0.35, 1), infinite */
02

Indeterminate

When there's nothing to measure yet, leave out value. Radix then marks the indicator data-state="indeterminate", and GodUI turns it into a sweep.

The indicator snaps to 40% of the track, once, when it becomes indeterminate. The keyframe then moves it from one full bar-width left of the track (-100%) to two and a half bar-widths right (250%, which is exactly the track's far edge, since 2.5 × 40% = 100%). A CSS animation outranks the inline transform, so the value-driven slide stays out of the way. The easing is a symmetric ease-in-out, cubic-bezier(0.65, 0, 0.35, 1): the bar eases in from off the left edge, is at full speed while it crosses the track, and eases out past the right edge. Some of the bar is on the track for almost 90% of each loop; it's empty for under 200ms between one sweep and the next.

03/03Result
Account setup25%
03

The result

Press Next step and Back to move the fill. The lower bar has no value, so it keeps sweeping.

What's animated

InteractionKeyframe / mechanismPropertiesEasingDuration
Value changetransitiontransformease-spring-smooth380ms
Indeterminategodui-progress-indeterminatetransformcubic-bezier(0.65, 0, 0.35, 1) (ease-in-out)1.4s loop

Why GPU-only

Both animations move transform. The fill never changes size; the indeterminate bar changes its width once, as a snap, and then only slides. shadcn's transition-all is gone, so nothing else on the bar can tween.

Reduced motion

motion-reduce:transition-none makes a new value snap into place. An indeterminate bar stops sweeping: it fills the whole track and pulses its opacity, so it still reads as busy without moving.

Replacing shadcn

npx shadcn add @godui/progress overwrites components/ui/progress.tsx. Exports, props and data-slot attributes match shadcn/ui new-york-v4, so existing imports keep working. It also installs godui-motion (easings, keyframes, the FLIP hook) into your project, plus the godui-progress-indeterminate keyframe.

GodUI also fixes a shadcn bug: shadcn reads value but never hands it to Radix, so its bar has no aria-valuenow and always reports data-state="indeterminate". GodUI passes value through, so screen readers hear the percentage and only a missing value counts as indeterminate.