AdminKitDocs
Components

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.

Pro

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

ClassEffect
progressThe 1rem high track, with role="progressbar" and the aria-value* attributes
progress-barThe filled part, as wide as the value
progress-sm, progress-lgAdminKit: a 0.4rem or 1.5rem high track
bg-<color>The bar color: bg-success, bg-warning, ...
progress-bar-stripedStripes over the color
progress-bar-animatedMoving stripes
progress-stackedSeveral .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:

src/components/storage-usage.tsx
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 with aria-labelledby pointing at its visible label.
  • Compute aria-valuenow and 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-valuetext with that text.
  • Text on a .progress-bar.bg-warning is dark in AdminKit PRO, because white text does not reach a 4.5:1 contrast on it.

Last updated on

On this page