termino
~/docs/custom/progress-bar

ProgressBarcustom

Animated progress bar with label and percentage

~/custom/progress-bar
live
$ termino progress-bar
deploy
0
push
0
release
0
tick 0 · useTimeline → onUpdate

##Variants

blocks · Solid background-color fill (default)
~/custom/progress-bar · blocks
$ termino progress-bar
deploy
0
push
0
release
0
tick 0 · useTimeline → onUpdate
line · Box-drawing ━/─ track glyphs
~/custom/progress-bar · line
$ termino progress-bar
deploy
0
push
0
release
0
tick 0 · useTimeline → onUpdate
dots · Dot-matrix ●/· style
~/custom/progress-bar · dots
$ termino progress-bar
deploy
······················ 0
push
······················ 0
release
······················ 0
tick 0 · useTimeline → onUpdate
gradient · Fill lerps toward white across the track
~/custom/progress-bar · gradient
$ termino progress-bar
deploy
0
push
0
release
0
tick 0 · useTimeline → onUpdate

##API

Composition-only component: `<ProgressBar />` builds on `<box>` + `<text>`. No `extend()` registration needed.

properties
proptypedefaultdescription
valuenumber-Current value (clamped to [0, max])
maxnumber100Maximum value
widthnumber20Track width in characters
labelstring-Optional label on the left of the row
colorstring"#7aa2f7"Fill color
trackColorstring"#2f3449"Track color
percentColorstring"#565f89"Percentage text color
showPercentbooleantrueShow the percentage value
variant"blocks" | "line" | "dots" | "gradient""blocks"Bar rendering style

##Keymap

~/keymap
No keyboard input (display only)
value propDrive progress from state or useTimeline

##Notes

  • Fill is a nested `<box>` with computed width — the same pattern the system-monitor example uses.
  • Animate by feeding it state updated from useTimeline's onUpdate, or plain setInterval.

##Source

Real implementation — this is the code that runs in your terminal.

tsxprogress-bar.tsx
1import { createElement as h } from "react";
2import { mixColor } from "./chart";
3
4export type ProgressBarVariant = "blocks" | "line" | "dots" | "gradient";
5
6export interface ProgressBarProps {
7 value: number;
8 max?: number;
9 width?: number;
10 label?: string;
11 color?: string;
12 trackColor?: string;
13 percentColor?: string;
14 showPercent?: boolean;
15 variant?: ProgressBarVariant;
16}
17
18const VARIANT_GLYPHS: Record<Exclude<ProgressBarVariant, "blocks">, [string, string]> = {
19 line: ["━", "─"],
20 dots: ["●", "·"],
21 gradient: ["█", "░"],
22};
23
24/** Colour of one filled cell. The gradient variant lightens towards the right
25 * end of the bar; every other variant paints a flat colour. */
26function fillColor(
27 variant: ProgressBarVariant,
28 color: string,
29 index: number,
30 width: number,
31): string {
32 if (variant !== "gradient") return color;
33 return mixColor(color, "#ffffff", (index / Math.max(1, width - 1)) * 0.55);
34}
35
36export function ProgressBar({
37 value,
38 max = 100,
39 width = 20,
40 label,
41 color = "#7aa2f7",
42 trackColor = "#2f3449",
43 percentColor = "#565f89",
44 showPercent = true,
45 variant = "blocks",
46}: Readonly<ProgressBarProps>) {
47 const clamped = Math.max(0, Math.min(max, value));
48 const pct = max === 0 ? 0 : Math.round((clamped / max) * 100);
49 const filled = Math.max(0, Math.min(width, Math.round((pct / 100) * width)));
50
51 const bar =
52 variant === "blocks"
53 ? h(
54 "box",
55 { style: { width: width + 4, height: 1, backgroundColor: trackColor } },
56 h("box", { style: { width: filled, height: 1, backgroundColor: color } }),
57 )
58 : h(
59 "box",
60 { flexDirection: "row" },
61 Array.from({ length: width }, (_, i) => {
62 const [fillCh, trackCh] = VARIANT_GLYPHS[variant];
63 const isFill = i < filled;
64 const fg = isFill ? fillColor(variant, color, i, width) : trackColor;
65 return h("text", { key: i, fg }, isFill ? fillCh : trackCh);
66 }),
67 );
68
69 const headerParts = [];
70 if (label) headerParts.push(h("text", { fg: "#565f89" }, label));
71 if (showPercent) headerParts.push(h("text", { fg: percentColor }, `${pct}%`));
72 const header = headerParts.length
73 ? h(
74 "box",
75 { flexDirection: "row", justifyContent: "space-between", width: width + 4 },
76 ...headerParts,
77 )
78 : null;
79
80 return h(
81 "box",
82 { flexDirection: "column", gap: 0 },
83 header,
84 bar,
85 );
86}
87