Use Progress when work is ongoing. It renders its track and indicator for you. Pass a number when completed work can be measured; pass null when it cannot.
Live preview
"use client";
import * as React from "react";
import { Progress } from "@/components/ui/progress";
export default function ProgressDemo() {
const [progress, setProgress] = React.useState(13);
React.useEffect(() => {
const timer = setTimeout(() => setProgress(66), 500);
return () => clearTimeout(timer);
}, []);
return <Progress value={progress} className="w-[60%]" aria-label="Loading progress" />;
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/progress.jsonnpm install @base-ui/react@^1.8.0Create components/ui/progress.tsx:
"use client";
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
import { cn } from "@/lib/utils";
type ProgressProps = Omit<ProgressPrimitive.Root.Props, "value"> & {
value?: number | null;
};
function Progress({ children, className, value = null, ...props }: ProgressProps) {
return (
<ProgressPrimitive.Root
data-slot="progress"
value={value}
{...props}
className={(state) =>
cn(
"flex w-full flex-wrap gap-3",
typeof className === "function" ? className(state) : className,
)
}
>
{children}
<ProgressTrack>
<ProgressIndicator />
</ProgressTrack>
</ProgressPrimitive.Root>
);
}
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
return (
<ProgressPrimitive.Track
data-slot="progress-track"
{...props}
className={(state) =>
cn(
"relative flex h-4 w-full items-center overflow-hidden rounded-base border-2 border-border bg-secondary-background",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
function ProgressIndicator({ className, ...props }: ProgressPrimitive.Indicator.Props) {
return (
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
{...props}
className={(state) =>
cn(
"h-full border-r-2 border-border bg-main transition-all",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
return (
<ProgressPrimitive.Label
data-slot="progress-label"
{...props}
className={(state) =>
cn("text-sm font-heading", typeof className === "function" ? className(state) : className)
}
/>
);
}
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
return (
<ProgressPrimitive.Value
data-slot="progress-value"
{...props}
className={(state) =>
cn(
"ml-auto text-sm font-base text-foreground tabular-nums",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
export { Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue };
#Usage
import { Progress } from "@/components/ui/progress";
export default function UploadProgress() {
return (
<div className="grid max-w-sm gap-2">
<p id="upload-label">Uploading files: 42%</p>
<Progress value={42} aria-labelledby="upload-label" />
</div>
);
}#API reference
| Prop | Purpose | Default |
|---|---|---|
value | Completed amount; null means indeterminate | null |
max | Maximum amount | 100 |
aria-label / aria-labelledby | Accessible operation name | Unset |
getAriaValueText | Meaningful value announcement | Base UI behavior |
className | Width and root layout | Full available width |
#Accessibility
Name the operation, not just the control. Do not invent percentages for unknown work. Keep values in range and announce completion or failure separately; reaching 100 does not prove that an application operation succeeded.