Progress
Shows how far along a task or quota is.
stableProgress.tsx
Examples
Tones
Storage72%
Bandwidth38%
Build minutes91%
progress/tones.tsx
import { Progress } from "@streamui/react";
const bars = [
{ label: "Storage", value: 72, tone: "pink" },
{ label: "Bandwidth", value: 38, tone: "lime" },
{ label: "Build minutes", value: 91, tone: "orange" },
] as const;
export default function ProgressTones() {
return (
<div className="flex w-full max-w-sm flex-col gap-5">
{bars.map((b) => (
<div key={b.label}>
<div className="mb-2 flex justify-between text-sm">
<span className="font-bold">{b.label}</span>
<span className="font-mono tabular-nums">{b.value}%</span>
</div>
<Progress value={b.value} tone={b.tone} aria-label={b.label} />
</div>
))}
</div>
);
}Steps
Use max for anything that isn't a percentage.
Onboarding: step 2 of 5
progress/interactive.tsx
"use client";
import { useState } from "react";
import { Minus, Plus } from "lucide-react";
import { Button, Progress } from "@streamui/react";
export default function ProgressInteractive() {
const [step, setStep] = useState(2);
const total = 5;
return (
<div className="w-full max-w-xs">
<p id="onboarding-label" className="mb-2 font-bold">
Onboarding: step {step} of {total}
</p>
<Progress value={step} max={total} tone="blue" aria-labelledby="onboarding-label" />
<div className="mt-5 flex gap-3">
<Button
size="icon"
variant="secondary"
className="size-10"
aria-label="Previous step"
disabled={step <= 0}
onClick={() => setStep((s) => s - 1)}
>
<Minus aria-hidden />
</Button>
<Button
size="icon"
variant="secondary"
className="size-10"
aria-label="Next step"
disabled={step >= total}
onClick={() => setStep((s) => s + 1)}
>
<Plus aria-hidden />
</Button>
</div>
</div>
);
}Installation
bun add @streamui/reactusage.tsx
import { Progress } from "@streamui/react";
<Progress value={60} aria-label="Upload" />API
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | — | Current value. Clamped between 0 and max. |
| max | number | 100 | Value that means complete. |
| tone | "lime" | "pink" | "blue" | "violet" | "orange" | "pink" | Fill color. |
| ...props | ComponentProps<"div"> | — | Native attributes; pass aria-label or aria-labelledby. |
Accessibility
- Uses role="progressbar" with aria-valuenow, aria-valuemin, and aria-valuemax.
- Give it a name with aria-label or aria-labelledby.
- The fill animates width only when motion is allowed.
Source
packages/react/src/Progress.tsx
import type { ComponentProps } from "react";
import { cn } from "./cn";
import { toneBg, type Tone } from "./tones";
export type ProgressProps = Omit<ComponentProps<"div">, "children"> & {
value: number;
max?: number;
tone?: Tone;
};
/** Determinate progress bar. Give it a name with `aria-label` or `aria-labelledby`. */
export function Progress({ value, max = 100, tone = "pink", className, ...props }: ProgressProps) {
const clamped = Math.min(max, Math.max(0, value));
const percent = max > 0 ? (clamped / max) * 100 : 0;
return (
<div
role="progressbar"
aria-valuenow={clamped}
aria-valuemin={0}
aria-valuemax={max}
className={cn("h-6 border-2 border-ink bg-paper-sunken shadow-brutal-sm", className)}
{...props}
>
<div
className={cn(
"h-full transition-[width] duration-200 ease-out motion-reduce:transition-none",
percent > 0 && percent < 100 && "border-r-2 border-ink",
toneBg[tone],
)}
style={{ width: `${percent}%` }}
/>
</div>
);
}