component — feedback
Progress Bar
Accessible progress bar built on react-aria-components, styled with the quebi design system. A brand-teal fill over a cyan-tinted track, with optional header and value text, and a determinate or indeterminate state.
- progress
- loading
- status
- feedback
04 examples
Default
A determinate bar with a header label and value.
Track only
Just the bar, no header.
Indeterminate
An unknown-duration loading state.
Animated
Value driven over time to show the fill transition.
source
progress-bar.tsxCopy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import { createContext, use } from "react"
import {
composeRenderProps,
ProgressBar as ProgressBarPrimitive,
type ProgressBarProps,
type ProgressBarRenderProps,
} from "react-aria-components"
import { cn } from "@/lib/utils"
/**
* ProgressBar — quebi design system
*
* Built on react-aria-components. A thin square track in `hairline` with a
* `signal` fill — progress is state — that animates its width. Supports determinate and
* indeterminate states, plus an optional header with label and value — the
* value set in mono, like every count in the system.
*/
const ProgressBarContext = createContext<ProgressBarRenderProps | null>(null)
export function ProgressBar({ className, children, ...props }: ProgressBarProps) {
return (
<ProgressBarPrimitive
data-slot="control"
className={composeRenderProps(className, (resolved) =>
cn(
"flex w-full flex-col gap-2",
"*:data-[slot=progress-bar-header]:font-medium",
resolved,
),
)}
{...props}
>
{(values) => (
<ProgressBarContext value={values}>
{typeof children === "function" ? children(values) : children}
</ProgressBarContext>
)}
</ProgressBarPrimitive>
)
}
export function ProgressBarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="progress-bar-header"
className={cn("flex items-center justify-between text-sm text-quebi-fg", className)}
{...props}
/>
)
}
export function ProgressBarValue({
className,
...props
}: Omit<React.ComponentProps<"span">, "children">) {
const ctx = use(ProgressBarContext)
if (!ctx) throw new Error("ProgressBarValue must be used within a ProgressBar")
const { valueText } = ctx
return (
<span
data-slot="progress-bar-value"
className={cn("font-mono text-quebi-caption text-quebi-fg-subtle tabular-nums", className)}
{...props}
>
{valueText}
</span>
)
}
export function ProgressBarTrack({ className, ref, ...props }: React.ComponentProps<"div">) {
const ctx = use(ProgressBarContext)
if (!ctx) throw new Error("ProgressBarTrack must be used within a ProgressBar")
const { isIndeterminate, percentage } = ctx
return (
<span data-slot="progress-bar-track" className="relative block w-full">
<style>{`
@keyframes quebi-progress-slide {
0% { inset-inline-start: -40% }
100% { inset-inline-start: 100% }
}
`}</style>
<div
ref={ref}
data-slot="progress-container"
className={cn(
"relative h-1 w-full min-w-52 overflow-hidden bg-quebi-hairline will-change-transform",
className,
)}
{...props}
>
{!isIndeterminate ? (
<div
data-slot="progress-content"
className="absolute start-0 top-0 h-full bg-quebi-signal transition-[width] duration-200 ease-linear will-change-[width] motion-reduce:transition-none forced-colors:bg-[Highlight]"
style={{ width: `${percentage}%` }}
/>
) : (
<div
data-slot="progress-content"
className="absolute top-0 h-full w-2/5 animate-[quebi-progress-slide_1500ms_ease-in-out_infinite] bg-quebi-signal forced-colors:bg-[Highlight]"
/>
)}
</div>
</span>
)
}