byDefaultHuman
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.json

Props

PropTypeDefaultDescription
className
string
—
Additional Tailwind classes applied to the component.
id
string
—
Optional stable id used for sketch seeding and accessibility.
label
string
—
Optional label text shown with the component.
max
number
100
Upper bound for the value.
showValue
boolean
false
Shows the current numeric value in the UI.
theme
CrumbleTheme
—
Overrides the global Crumble theme for this instance.
value
number
0
Controlled 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>
  );
}