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.json

The source is copied into components/panelui/, where you can edit it.

npm install panelui-web

Import 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-variants

Copy 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

PropTypeDefaultDescription
dataBarPickerBar[]—The columns, left to right.
maxnumber—The value a fully lit column stands for. Defaults to the largest in data.
valuenumber—The index of the picked column, when the caller holds it.
defaultValuenumber0The 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.
segmentsnumber8How many lights a column is made of.
colorBarPickerColor'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".
labelstring'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.

On this page