COMPONENT / segmented-progress
Segmented Progress_
Determinate progress bar of hard --signal segments under a big padded percentage and a phase label. Requires @cybr/tokens.
Install
1. Map the
@cybrnamespace, once per project. Add this inside yourcomponents.json, or into its existing"registries"object. Nocomponents.jsonyet? Runnpx shadcn@latest initfirst to create one."registries": { "@cybr": "https://cybr-ui.com/r/{name}.json" }2. Add components:
$ npx shadcn@latest add @cybr/segmented-progress
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
HANDSHAKE
registry/ui/segmented-progress.tsx
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";
function finiteOr(n: number, fallback: number) {
return Number.isFinite(n) ? n : fallback;
}
// Determinate progress as a row of hard segments under a big padded readout.
// The track is the progressbar; the readout repeats its value, so it is
// aria-hidden. Segments snap on and off with no animation.
export function SegmentedProgress({
value,
max = 100,
segments = 20,
label,
phase,
showValue = true,
className,
...props
}: Omit<ComponentProps<"div">, "children"> & {
value: number;
max?: number;
segments?: number;
label: string;
phase?: ReactNode;
showValue?: boolean;
}) {
const limit = finiteOr(max, 100) > 0 ? max : 100;
const count = Math.max(1, Math.floor(finiteOr(segments, 20)));
const current = Math.min(limit, Math.max(0, finiteOr(value, 0)));
const filled = Math.floor((current / limit) * count);
const percent = Math.round((current / limit) * 100);
return (
<div className={cn("flex flex-col gap-3 font-mono text-ink", className)} {...props}>
{(showValue || phase) && (
<div className="flex items-baseline justify-between gap-4">
{showValue ? (
<span aria-hidden="true" className="font-display text-[4rem] leading-[0.9] tabular-nums">
{String(percent).padStart(3, "0")}%
</span>
) : (
<span />
)}
{phase && <span className="text-caption tracking-label text-label">{phase}</span>}
</div>
)}
<div
role="progressbar"
aria-label={label}
aria-valuemin={0}
aria-valuemax={limit}
aria-valuenow={current}
className="grid h-5.5 gap-1 border-(length:--border-base) border-ink p-0.75"
style={{ gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }}
>
{Array.from({ length: count }, (_, i) => (
<span key={i} className={i < filled ? "bg-signal" : undefined} />
))}
</div>
</div>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | — | Current progress. Clamped to 0 to max; a value that is not finite counts as 0. |
| max | number | 100 | The value at full. Values that are not finite or not above 0 fall back to 100. |
| segments | number | 20 | Number of segments in the track, at least 1. |
| label | string | — | Required accessible name for the progressbar, such as Loading or Uploading assets. |
| phase | ReactNode | — | Short status on the right of the readout, such as HANDSHAKE. |
| showValue | boolean | true | Shows the big 3-digit percentage readout. |
| className | string | — | Extra classes, merged after the defaults with cn(). |
| ...props | Omit<ComponentProps<"div">, "children"> | — | Every other native <div> attribute and ref is forwarded. |
Usage
Needs the stylesheet from @cybr/tokens. After the install, import it in app/globals.css:
@import "tailwindcss";
@import "../styles/cybr-tokens.css";
import { SegmentedProgress } from "@/components/ui/segmented-progress";
export function Upload({ done }: { done: number }) {
return <SegmentedProgress value={done} label="Uploading assets" phase="TRANSFER" />;
}
States
- Empty:
000%over an--inkframed track with no lit segments. - Partial:
floor(value / max × segments)segments lit in--signal; the readout rounds to the nearest whole percent and pads to 3 digits. - Full:
100%and every segment lit. - Out of range: values below 0 show as 0, above
maxas full. - Segments snap on and off. Nothing animates, so there is no reduced-motion variant.
Accessibility
- The track is
role="progressbar"witharia-valuemin,aria-valuemax,aria-valuenowand yourlabelas its name. - The big readout is
aria-hidden, because the progressbar already exposes the value. The phase text stays readable. - This is determinate only. For unknown durations, show a Boot Sequence Loader or your own status text instead.
--inkon--bgis 14.6:1 and--labelon--bgis 7.43:1. Lit--signalsegments are not text, but keep them at 3:1 against the background (WCAG 1.4.11) if you override colours.