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>
  );
}