Components
Progress
A hand-drawn progress bar with animated hachure fill.
Animated Demo
Loading0%
Installation
package manager
npx shadcn add https://bydefaulthuman.fun/r/progress.jsonProps
PropTypeDefaultDescription
classNamestring—
Additional Tailwind classes applied to the component.
idstring—
Optional stable id used for sketch seeding and accessibility.
labelstring—
Optional label text shown with the component.
maxnumber100Upper bound for the value.
showValuebooleanfalseShows the current numeric value in the UI.
themeCrumbleTheme—
Overrides the global Crumble theme for this instance.
valuenumber0Controlled value for the component.
formatValue(value: number, max: number) => string—
Formats the displayed numeric value.
Static Example
With label and value
Uploading72%
Custom format
Use formatValue to control how the displayed value is rendered. Pass it from a client component to avoid the server/client serialization boundary.
Storage3.2 GB
Accessibility
Progress exposes the value visually, but you should still provide a meaningful label when the progress represents a task or upload state. Keep the value text enabled when the percentage itself matters.
Examples
The demo auto-plays on mount using requestAnimationFrame with a cubic ease-out
curve, so the fill decelerates naturally as it approaches 100 %. Hit Replay ↺
to restart.
// components/demos/ProgressDemo.tsx
"use client";
import { useEffect, useRef, useState } from "react";
import { Progress } from "@/registry/new-york/ui/progress";
export function ProgressDemo() {
const [value, setValue] = useState(0);
const rafRef = useRef<number | null>(null);
const startTimeRef = useRef<number | null>(null);
const DURATION = 2200; // ms to go 0 → 100
const runAnimation = () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
setValue(0);
startTimeRef.current = null;
const step = (now: number) => {
if (!startTimeRef.current) startTimeRef.current = now;
const elapsed = now - startTimeRef.current;
const pct = Math.min(elapsed / DURATION, 1);
// ease-out cubic
const eased = 1 - Math.pow(1 - pct, 3);
setValue(Math.round(eased * 100));
if (pct < 1) {
rafRef.current = requestAnimationFrame(step);
}
};
rafRef.current = requestAnimationFrame(step);
};
useEffect(() => {
runAnimation();
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<div className="flex flex-col items-start gap-4 w-full max-w-sm">
<Progress label="Loading" value={value} showValue />
<button
onClick={runAnimation}
className="self-start rounded-full border border-border px-3 py-1 text-xs text-muted-foreground hover:text-foreground hover:border-foreground/40 transition-colors"
>
Replay ↺
</button>
</div>
);
}