Progress
Progress for a useful, keyboard-friendly product interface.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/progress.jsonUse it
Scroll horizontally for long lines
"use client";
import { useState, useEffect } from "react";
import { Progress } from "@/components/jez-ui/ui/progress";
import { Button } from "@/components/jez-ui/ui/button";
import { Pause, Play, RotateCcw } from "lucide-react";
export default function Example() {
const [progress, setProgress] = useState(0);
const [progressRunning, setProgressRunning] = useState(false);
useEffect(() => {
if (!progressRunning) return;
if (progress >= 100) {
setProgressRunning(false);
return;
}
const timer = setTimeout(
() => setProgress((v) => Math.min(100, v + 4)),
200,
);
return () => clearTimeout(timer);
}, [progressRunning, progress]);
return (
<>
<div className="w-full max-w-sm">
<div className="mb-7">
<h3 className="text-2xl tracking-tight">Getting there.</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
The next release is taking shape.
</p>
</div>
<div className="grid gap-4">
<div className="flex items-center justify-between text-sm">
<span>
{progress === 100
? "Export complete"
: progressRunning
? "Preparing export…"
: progress > 0
? "Export paused"
: "Ready to export"}
</span>
<span className="font-mono tabular-nums">{progress}%</span>
</div>
<Progress
value={progress}
label="Export progress"
showLabel={false}
/>
<div className="flex gap-2">
<Button
size="sm"
disabled={progress === 100}
onClick={() => setProgressRunning((v) => !v)}
>
{progressRunning ? <Pause size={14} /> : <Play size={14} />}{" "}
{progressRunning
? "Pause"
: progress > 0
? "Resume"
: "Start export"}
</Button>
<Button
size="sm"
variant="ghost"
aria-label="Reset progress"
onClick={() => {
setProgressRunning(false);
setProgress(0);
}}
>
<RotateCcw size={14} />
</Button>
</div>
<p className="text-xs text-muted-foreground">
Simulated export · try pausing halfway through.
</p>
</div>
</div>
</>
);
}
Props & types
Scroll horizontally for long lines
Omit<
React.ComponentProps<"div">,
keyof {
value: number;
label?: string;
className?: string;
showLabel?: boolean;
}
> & {
value: number;
label?: string;
className?: string;
showLabel?: boolean;
}Source
View 2 source files
registry/ui/progress.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
export function Progress({
children,
value,
label = "Progress",
className,
showLabel = true,
...rootProps
}: Omit<
React.ComponentProps<"div">,
keyof {
value: number;
label?: string;
className?: string;
showLabel?: boolean;
}
> & {
value: number;
label?: string;
className?: string;
showLabel?: boolean;
}) {
const v = Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0;
return (
<div {...rootProps} className={cn("grid w-full min-w-0 gap-2", className)}>
{children !== undefined ? (
children
) : (
<>
{showLabel && (
<div className="flex justify-between text-sm">
<span>{label}</span>
<span className="tabular-nums">{Math.round(v)}%</span>
</div>
)}
<div
role="progressbar"
aria-label={label}
aria-valuenow={v}
aria-valuemin={0}
aria-valuemax={100}
className="h-2 overflow-hidden rounded-full bg-muted"
>
<div
className="h-full origin-left rounded-full bg-primary transition-transform duration-500 ease-out motion-reduce:transition-none"
style={{ transform: `scaleX(${v / 100})` }}
/>
</div>
</>
)}
</div>
);
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
Dependencies
clsx@2.1.1 · tailwind-merge@3.5.0
Accessibility & behaviour
Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.
Read the accessibility and performance guide →