Components · Feedback
Progress
A labelled progress bar, or a sweep while the amount is unknown that stands still under reduced motion.
Uploading assets64%
Storage used72%
Building
Uploading assets64%
Storage used72%
Building
Both previews render the same markup; the HTML tab below is that markup without a framework.
Install
- React
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ progress.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ progress.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.
import { cn } from '@/lib/utils';
import { progress } from './progress.variants';
export interface ProgressProps {
/** The visible label, also the bar's accessible name. */
label: string;
/** 0 to max. Leave it out while the amount is unknown. */
value?: number;
max?: number;
/** Unique on the page; ids the label. */
id: string;
className?: string;
}
/** A labelled progress bar (role=progressbar); without a value it shows that work is under way. */
export function Progress({ label, value, max = 100, id, className }: ProgressProps) {
const pct = value === undefined ? undefined : Math.round((Math.min(max, Math.max(0, value)) / max) * 100);
return (
<div className={cn(progress.root, className)}>
<div className={progress.head}>
<span id={`${id}-label`} className={progress.label}>{label}</span>
{pct !== undefined && <span className={progress.value}>{pct}%</span>}
</div>
<div role="progressbar" aria-labelledby={`${id}-label`} aria-valuemin={0} aria-valuemax={max} aria-valuenow={value} className={progress.track}>
{pct === undefined ? <div className={progress.indeterminate}></div> : <div className={progress.bar} style={{ width: `${pct}%` }}></div>}
</div>
</div>
);
}/** Progress: a labelled bar with role=progressbar and aria-valuenow (or none, while the amount is unknown). The fill
* eases to its value; an unknown amount sweeps across, and under reduced motion it is a still, half-filled bar. */
export const progress = {
root: 'grid w-full gap-2',
head: 'flex items-baseline justify-between gap-4 text-sm',
label: 'font-medium',
value: 'text-muted-foreground tabular-nums',
track: 'relative h-2 w-full overflow-hidden rounded-full bg-muted',
bar: 'h-full rounded-full bg-primary transition-[width] duration-500 ease-out',
indeterminate:
'absolute inset-y-0 w-2/5 rounded-full bg-primary animate-[caseroom-sweep_1.4s_ease-in-out_infinite] motion-reduce:static motion-reduce:w-1/2 motion-reduce:animate-none',
};---
import { cn } from '@/lib/utils';
import { progress } from './progress.variants';
interface Props {
/** The visible label, also the bar's accessible name. */
label: string;
/** 0 to max. Leave it out while the amount is unknown. */
value?: number;
max?: number;
/** Unique on the page; ids the label. */
id: string;
class?: string;
}
const { label, value, max = 100, id, class: className } = Astro.props;
const pct = value === undefined ? undefined : Math.round((Math.min(max, Math.max(0, value)) / max) * 100);
---
<div class={cn(progress.root, className)}>
<div class={progress.head}>
<span id={`${id}-label`} class={progress.label}>{label}</span>
{pct !== undefined && <span class={progress.value}>{pct}%</span>}
</div>
<div role="progressbar" aria-labelledby={`${id}-label`} aria-valuemin="0" aria-valuemax={max} aria-valuenow={value} class={progress.track}>
{pct === undefined ? <div class={progress.indeterminate}></div> : <div class={progress.bar} style={`width: ${pct}%`}></div>}
</div>
</div>/** Progress: a labelled bar with role=progressbar and aria-valuenow (or none, while the amount is unknown). The fill
* eases to its value; an unknown amount sweeps across, and under reduced motion it is a still, half-filled bar. */
export const progress = {
root: 'grid w-full gap-2',
head: 'flex items-baseline justify-between gap-4 text-sm',
label: 'font-medium',
value: 'text-muted-foreground tabular-nums',
track: 'relative h-2 w-full overflow-hidden rounded-full bg-muted',
bar: 'h-full rounded-full bg-primary transition-[width] duration-500 ease-out',
indeterminate:
'absolute inset-y-0 w-2/5 rounded-full bg-primary animate-[caseroom-sweep_1.4s_ease-in-out_infinite] motion-reduce:static motion-reduce:w-1/2 motion-reduce:animate-none',
};<div class="grid w-full max-w-sm gap-6">
<div class="grid w-full gap-2">
<div class="flex items-baseline justify-between gap-4 text-sm">
<span id="ex-upload-label" class="font-medium">Uploading assets</span>
<span class="text-muted-foreground tabular-nums">64%</span>
</div>
<div role="progressbar" aria-labelledby="ex-upload-label" aria-valuemin="0" aria-valuemax="100" aria-valuenow="64" class="relative h-2 w-full overflow-hidden rounded-full bg-muted">
<div class="h-full rounded-full bg-primary transition-[width] duration-500 ease-out" style="width: 64%"></div>
</div>
</div>
<div class="grid w-full gap-2">
<div class="flex items-baseline justify-between gap-4 text-sm">
<span id="ex-storage-label" class="font-medium">Storage used</span>
<span class="text-muted-foreground tabular-nums">72%</span>
</div>
<div role="progressbar" aria-labelledby="ex-storage-label" aria-valuemin="0" aria-valuemax="10" aria-valuenow="7.2" class="relative h-2 w-full overflow-hidden rounded-full bg-muted">
<div class="h-full rounded-full bg-primary transition-[width] duration-500 ease-out" style="width: 72%"></div>
</div>
</div>
<div class="grid w-full gap-2">
<div class="flex items-baseline justify-between gap-4 text-sm"><span id="ex-build-label" class="font-medium">Building</span></div>
<div role="progressbar" aria-labelledby="ex-build-label" aria-valuemin="0" aria-valuemax="100" class="relative h-2 w-full overflow-hidden rounded-full bg-muted">
<div class="absolute inset-y-0 w-2/5 rounded-full bg-primary animate-[caseroom-sweep_1.4s_ease-in-out_infinite] motion-reduce:static motion-reduce:w-1/2 motion-reduce:animate-none">
</div>
</div>
</div>
</div>