Bar Picker
A short run of values as columns of lights, one column picked.
'use client';
import { useState } from 'react';
import { BarPicker, type BarPickerBar } from '@/components/panelui/bar-picker';
/** Deploys to production on each day of one week. */
const WEEK: BarPickerBar[] = [
{ label: 'Mon', name: 'Monday', value: 9 },
{ label: 'Tue', name: 'Tuesday', value: 14 },
{ label: 'Wed', name: 'Wednesday', value: 11 },
{ label: 'Thu', name: 'Thursday', value: 17 },
{ label: 'Fri', name: 'Friday', value: 6 },
{ label: 'Sat', name: 'Saturday', value: 0 },
{ label: 'Sun', name: 'Sunday', value: 2 },
];
const TOTAL = WEEK.reduce((sum, day) => sum + day.value, 0);
export default function BarPickerDeploys() {
const [day, setDay] = useState(3);
const picked = WEEK[day];
const share = Math.round((picked.value / TOTAL) * 100);
return (
// A frame: a band that names what is inside, round a panel that holds it.
<div className="w-full max-w-sm rounded-3xl bg-muted p-1.5">
<div className="flex items-center justify-between gap-3 px-3.5 py-2.5">
<h3 className="text-sm font-medium">Deploys</h3>
<p className="text-sm text-muted-foreground">This week · {TOTAL}</p>
</div>
<div className="flex flex-col gap-6 rounded-[1.125rem] border bg-card p-5 text-card-foreground shadow-xs">
<div className="flex items-end justify-between gap-4" aria-live="polite">
<div className="flex flex-col gap-1">
<p className="text-sm text-muted-foreground">{picked.name}</p>
<p className="text-4xl font-bold leading-none tracking-tight">{picked.value}</p>
</div>
<p className="rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
{share}% of the week
</p>
</div>
<BarPicker
data={WEEK}
value={day}
onValueChange={setDay}
color="info"
label="Deploys by day"
formatValue={(value) => `${value} deploys`}
/>
</div>
</div>
);
}Use it for a handful of values where one is in focus: a week with one day picked, six sprints with the latest picked. Each value is a column of lights, lit from the bottom up to the value. Pressing a column picks it, and you show that column's figure beside the row.
It behaves as a row of radio buttons, so exactly one column is always picked.
A column of eight lights shows a value to the nearest eighth. For values someone has to read precisely, for more than about twelve of them, or for more than one series, use a chart with an axis.
Installation
npx shadcn@latest add https://panelui.dev/r/web/bar-picker.jsonpnpm dlx shadcn@latest add https://panelui.dev/r/web/bar-picker.jsonyarn dlx shadcn@latest add https://panelui.dev/r/web/bar-picker.jsonbunx shadcn@latest add https://panelui.dev/r/web/bar-picker.jsonThe source is copied into components/panelui/, where you can edit it.
npm install panelui-webpnpm add panelui-webyarn add panelui-webbun add panelui-webImport from panelui-web/bar-picker instead of @/components/panelui/bar-picker. Tailwind needs one line to read the package's classes, which is on the installation page.
Install the packages it uses:
npm install tailwind-variantspnpm add tailwind-variantsyarn add tailwind-variantsbun add tailwind-variantsCopy the source into components/panelui/bar-picker.tsx:
Usage
import { BarPicker, type BarPickerBar } from "@/components/panelui/bar-picker"const week: BarPickerBar[] = [
{ label: 'Mon', name: 'Monday', value: 9 },
{ label: 'Tue', name: 'Tuesday', value: 14 },
{ label: 'Wed', name: 'Wednesday', value: 11 },
]
<BarPicker data={week} value={day} onValueChange={setDay} />Examples
A fixed scale and a colour
max sets what a fully lit column stands for, here 100 percent, so the columns are lit against the whole scale and not against the tallest of them. defaultValue picks a column to begin with and lets the component hold the choice.
'use client';
import { BarPicker, type BarPickerBar } from '@/components/panelui/bar-picker';
/** Test coverage at the end of each sprint, as a percentage. */
const SPRINTS: BarPickerBar[] = [
{ label: 'S1', name: 'Sprint 1', value: 52 },
{ label: 'S2', name: 'Sprint 2', value: 58 },
{ label: 'S3', name: 'Sprint 3', value: 57 },
{ label: 'S4', name: 'Sprint 4', value: 66 },
{ label: 'S5', name: 'Sprint 5', value: 74 },
{ label: 'S6', name: 'Sprint 6', value: 81 },
];
export default function BarPickerCoverage() {
return (
<BarPicker
data={SPRINTS}
max={100}
defaultValue={5}
color="success"
label="Coverage by sprint"
formatValue={(value) => `${value} percent`}
className="max-w-xs"
/>
);
}API Reference
BarPicker
| Prop | Type | Default | Description |
|---|---|---|---|
data | BarPickerBar[] | — | The columns, left to right. |
max | number | — | The value a fully lit column stands for. Defaults to the largest in data. |
value | number | — | The index of the picked column, when the caller holds it. |
defaultValue | number | 0 | The index picked to begin with, when the caller does not hold it. |
onValueChange | (index: number) => void | — | Called with the index of a column when it is picked. |
segments | number | 8 | How many lights a column is made of. |
color | BarPickerColor | 'primary' | Which of the theme's roles the lights are drawn in. |
formatValue | (value: number) => string | (bar) => String(bar) | Writes a value for a screen reader: "17 deploys". |
label | string | 'Values' | What a screen reader calls the row. |
Every part also takes the props of the element it renders, and a className.
Notes
Scale
A column lights the share of its segments that its value is of max, rounded to the nearest segment. Leave max out to scale to the largest value in data, or set it when the values share a known ceiling.
Any value above zero lights at least one segment. A value of zero lights none, and a value above max lights them all.
Segments
A column has 8 segments unless segments says otherwise. More segments show a value more finely and need more height to stay readable. The stack is 7rem tall.
Picking
The whole column is the target, so an empty column can still be picked. The picked column is half as wide again as the others, lit at full strength, and its highest light glows. The others are lit at about a quarter of the same colour.
Labels
label is drawn under the column and should be a few characters. name is the long form a screen reader is given with the value: "Thursday: 17 deploys". Use formatValue to say the unit.
Keyboard
The row is one Tab stop. Left and right move the pick and wrap at the ends, and Home and End go to the first and last column.
Motion
The columns light from the bottom when the component mounts, and the picked column widens as the pick moves. With reduced motion on, both happen at once.