Next.js Bootstrap Progress Bar
A Next.js Bootstrap progress bar in AdminKit PRO: server-rendered quotas, a live value from client state, aria-valuenow, labels, colors and stacked bars.
This component is only available in AdminKit PRO. Learn more.
A progress bar tells people how far along something is: how much of a storage quota is used, or how much of an upload is done. It is Bootstrap's .progress markup in JSX, with no component to import. Progress bars are part of the PRO version of AdminKit for Next.js.
Where the value comes from decides where the bar lives. In the App Router, pages are server components: they render on the server and have no state. A bar whose value is known when the page renders, such as a quota on the settings page, is plain markup in a server component, with no "use client". A bar that moves while the user watches, such as an upload, needs state. It goes in a client component, a file that starts with "use client". Either way, one value sets both the width and the aria-valuenow.
This is a progress bar inside the page content. It is not a loading bar at the top of the window that runs on route changes: AdminKit for Next.js does not include one.
Example
export function UploadProgress({ value }: { value: number }) {
return (
<div
className="progress"
role="progressbar"
aria-label="Upload"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={100}
>
<div className="progress-bar" style={{ width: `${value}%` }} />
</div>
);
}UploadProgress uses no hooks, so it works in both places. A server component can render it with a fixed value, and a client component can pass it a value from state. The aria-value* props take numbers, and the width is a template string in a style object. When the value changes, Bootstrap animates the width.
Classes
| Class | Effect |
|---|---|
progress | The 1rem high track, with role="progressbar" and the aria-value* attributes |
progress-bar | The filled part, as wide as the value |
progress-sm, progress-lg | AdminKit: a 0.4rem or 1.5rem high track |
bg-<color> | The bar color: bg-success, bg-warning, ... |
progress-bar-striped | Stripes over the color |
progress-bar-animated | Moving stripes |
progress-stacked | Several .progress elements as segments of one bar |
Quota in a server component
A quota is usually known when the page renders, so its bar can stay in a server component. The billing card of the settings page renders its bars on the server. Each bar has a visible label that aria-labelledby points to. A value that is not a percentage, such as 18.4 GB of 100 GB, is converted first:
export function StorageUsage({ used, total }: { used: number; total: number }) {
const percent = Math.min(Math.round((used / total) * 100), 100);
return (
<>
<div className="d-flex justify-content-between mb-1">
<span id="storage-label">Storage</span>
<span className="text-body-secondary">
{used} GB of {total} GB
</span>
</div>
<div
className="progress progress-sm"
role="progressbar"
aria-labelledby="storage-label"
aria-valuenow={percent}
aria-valuemin={0}
aria-valuemax={100}
>
<div className="progress-bar" style={{ width: `${percent}%` }} />
</div>
</>
);
}Math.min(..., 100) clamps the value, as on the CRM dashboard, so a quota over 100% stays inside the track.
In a static export, the server render happens once, at build time. A value that changes after the build has to be fetched in the browser, in a client component.
Labels, colors and stripes
To show the value on the bar itself, put text inside .progress-bar. Keep it short, and only use it on bars that are wide enough. Colors and stripes are classes on the same element:
<div className="progress" role="progressbar" aria-label="Tasks done" aria-valuenow={25} aria-valuemin={0} aria-valuemax={100}>
<div className="progress-bar bg-success" style={{ width: "25%" }}>25%</div>
</div>
<div className="progress" role="progressbar" aria-label="Import" aria-valuenow={50} aria-valuemin={0} aria-valuemax={100}>
<div className="progress-bar progress-bar-striped" style={{ width: "50%" }} />
</div>
<div className="progress" role="progressbar" aria-label="Sync" aria-valuenow={75} aria-valuemin={0} aria-valuemax={100}>
<div className="progress-bar progress-bar-striped progress-bar-animated bg-info" style={{ width: "75%" }} />
</div>Stacked bars
To split one bar into parts, such as paid, pending and overdue, use .progress-stacked. It holds one .progress per segment, each with its own value and label:
const segments = [
{ label: "Paid", value: 45, color: "success" },
{ label: "Pending", value: 30, color: "warning" },
{ label: "Overdue", value: 10, color: "danger" },
];
<div className="progress-stacked">
{segments.map((segment) => (
<div
key={segment.label}
className="progress"
role="progressbar"
aria-label={segment.label}
aria-valuenow={segment.value}
aria-valuemin={0}
aria-valuemax={100}
style={{ width: `${segment.value}%` }}
>
<div className={`progress-bar bg-${segment.color}`} />
</div>
))}
</div>Accessibility
- Name every bar with
aria-label, or witharia-labelledbypointing at its visible label. - Compute
aria-valuenowand the width from the same variable, so both always agree. - For a value that is not a percentage, such as 18.4 GB, add
aria-valuetextwith that text. - Text on a
.progress-bar.bg-warningis dark in AdminKit PRO, because white text does not reach a 4.5:1 contrast on it.
Related
- React Bootstrap progress bar: the same markup in AdminKit for React (Vite)
- Bootstrap 5 progress: the progress bars of the HTML version
- Spinners: a loading state without a known value
- General UI demo
Last updated on
PopoversPro
A Next.js Bootstrap popover for App Router client components: a title and body that open on click beside an element and render only in the browser.
SpinnersPro
Next.js Bootstrap spinner markup in AdminKit PRO: border and grow spinners in server components, client-side loading states and where loading.tsx fits.