Progress
Progress bar that fills with scale instead of width, plus an indeterminate state for unknown durations.
Uploading video.mp4
20%
"use client";
import { useEffect, useState } from "react";
import { Progress } from "@/components/motion/progress";
export function ProgressPreview() {
const [value, setValue] = useState(20);
useEffect(() => {
const interval = setInterval(() => {
setValue((v) => (v >= 100 ? 20 : v + 20));
}, 1200);
return () => clearInterval(interval);
}, []);
return (
<div className="flex w-full max-w-xs flex-col gap-1.5 rounded-2xl bg-background p-4 text-sm shadow-[0_0_0_1px_var(--border)]">
<span className="font-medium text-foreground">Uploading video.mp4</span>
<Progress value={value} showValue />
</div>
);
}
// easeui.dev/components/motion/progress
import { cn } from "@/lib/utils";
export interface ProgressProps {
/** 0 to 100. Omitted, the bar runs indeterminate. */
value?: number;
/** Shows the percentage above the bar. Ignored when indeterminate. Default false. */
showValue?: boolean;
className?: string;
}
/** A progress bar that fills with scale, not width, so it stays cheap to animate. */
export function Progress({ value, showValue = false, className }: ProgressProps) {
const clamped = value === undefined ? undefined : Math.min(100, Math.max(0, value));
return (
<div className={cn("flex flex-col gap-1.5", className)}>
{showValue && clamped !== undefined ? (
<span className="text-right text-xs tabular-nums text-muted-foreground">
{Math.round(clamped)}%
</span>
) : null}
<div
role="progressbar"
aria-valuenow={clamped}
aria-valuemin={0}
aria-valuemax={100}
className="h-2 w-full overflow-hidden rounded-full bg-muted shadow-[0_0_0_1px_var(--border)]"
>
<div
className={cn(
"h-full w-full origin-left rounded-full bg-foreground",
clamped === undefined
? "scale-x-[0.4] animate-progress-indeterminate motion-reduce:animate-none"
: "transition-transform duration-300 ease-out motion-reduce:transition-none",
)}
style={clamped === undefined ? undefined : { transform: `scaleX(${clamped / 100})` }}
/>
</div>
</div>
);
}
Installation
$ bunx --bun shadcn add @easeui/progress
- 1
Set up the theme tokens
Do this once per project. Follow the theme setup or skip it if you already ran shadcn init.
- 2
Install the dependencies
terminal npm install clsx tailwind-merge - 3
Add the source files
components/motion/progress.tsx // easeui.dev/components/motion/progress import { cn } from "@/lib/utils"; export interface ProgressProps { /** 0 to 100. Omitted, the bar runs indeterminate. */ value?: number; /** Shows the percentage above the bar. Ignored when indeterminate. Default false. */ showValue?: boolean; className?: string; } /** A progress bar that fills with scale, not width, so it stays cheap to animate. */ export function Progress({ value, showValue = false, className }: ProgressProps) { const clamped = value === undefined ? undefined : Math.min(100, Math.max(0, value)); return ( <div className={cn("flex flex-col gap-1.5", className)}> {showValue && clamped !== undefined ? ( <span className="text-right text-xs tabular-nums text-muted-foreground"> {Math.round(clamped)}% </span> ) : null} <div role="progressbar" aria-valuenow={clamped} aria-valuemin={0} aria-valuemax={100} className="h-2 w-full overflow-hidden rounded-full bg-muted shadow-[0_0_0_1px_var(--border)]" > <div className={cn( "h-full w-full origin-left rounded-full bg-foreground", clamped === undefined ? "scale-x-[0.4] animate-progress-indeterminate motion-reduce:animate-none" : "transition-transform duration-300 ease-out motion-reduce:transition-none", )} style={clamped === undefined ? undefined : { transform: `scaleX(${clamped / 100})` }} /> </div> </div> ); }lib/utils.ts import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| value? | number | - | 0 to 100. Omitted, the bar runs indeterminate. |
| showValue? | boolean | false | Shows the percentage above the bar. Ignored when indeterminate. Default false. |
| className? | string | - | - |
Updated