# Bar List

Rank labeled values with a bar for each, relative to the largest.

Top pages by visitors

[Complete TSX file](/examples/bar-list/default.tsx)

```tsx
"use client";

import { BarList } from "@/siteplane/components/bar-list";

const pages = [
  { id: "pricing", label: "/pricing", value: 8420 },
  { id: "installation", label: "/docs/installation", value: 5160 },
  { id: "release-notes", label: "/blog/release-notes", value: 2380 },
  { id: "changelog", label: "/changelog", value: 1240 },
  { id: "about", label: "/about", value: 615 },
  { id: "careers", label: "/careers", value: 48 },
];

export default function Example() {
  return (
    <div className="w-full max-w-md">
      <BarList data={pages} label="Top pages by visitors" />
    </div>
  );
}
```

A bar list ranks a short list of labeled values, such as top pages or traffic sources. Each row writes its label over a bar and its value at the end. The largest value fills the track and every other bar takes its share of it, so the rows compare at a glance. The rows keep the order of `data`, so sort the data to rank it.

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/bar-list
```

With npm: `npx shadcn@4.18.0 add @siteplane/bar-list`. [Registry JSON](/registry/bar-list.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/toggle-group
```

## Usage

```tsx
import { BarList } from "@/siteplane/components/bar-list";

<BarList
  label="Top pages"
  data={[
    { id: "pricing", label: "/pricing", value: 8420 },
    { id: "installation", label: "/docs/installation", value: 5160 },
  ]}
/>;
```

## Formatted values

Values are written as numbers in the format of `locale`, `en-US` by default, such as 8,420. Pass `formatValue` to write them another way, such as a currency or with a unit. A value with digits rolls to its new value when the data changes; text without digits is written as it is.

Revenue by country in US dollars

[Complete TSX file](/examples/bar-list/formatted.tsx)

```tsx
"use client";

import { BarList } from "@/siteplane/components/bar-list";

const revenue = [
  { id: "us", label: "United States", value: 48250 },
  { id: "de", label: "Germany", value: 21480 },
  { id: "gb", label: "United Kingdom", value: 17935 },
  { id: "ca", label: "Canada", value: 9120 },
  { id: "au", label: "Australia", value: 6340 },
];

const dollars = new Intl.NumberFormat("en-US", {
  currency: "USD",
  maximumFractionDigits: 0,
  style: "currency",
});

export default function Example() {
  return (
    <div className="w-full max-w-md">
      <BarList
        data={revenue}
        formatValue={(value) => dollars.format(value)}
        label="Revenue by country"
      />
    </div>
  );
}
```

## Icons

An `icon` sits before the label of its row. Pass an svg element, such as a lucide icon, to take the icon size of the kit. The icon is decoration and hidden from assistive technology, so the label must name the row on its own.

Traffic sources with icons

[Complete TSX file](/examples/bar-list/icons.tsx)

```tsx
"use client";

import {
  GlobeIcon,
  LinkIcon,
  MailIcon,
  SearchIcon,
  Share2Icon,
} from "lucide-react";
import { BarList } from "@/siteplane/components/bar-list";

const sources = [
  { icon: <SearchIcon />, id: "search", label: "Search", value: 5210 },
  { icon: <GlobeIcon />, id: "direct", label: "Direct", value: 3480 },
  { icon: <Share2Icon />, id: "social", label: "Social", value: 1265 },
  { icon: <MailIcon />, id: "email", label: "Email", value: 840 },
  { icon: <LinkIcon />, id: "referral", label: "Referral", value: 415 },
];

export default function Example() {
  return (
    <div className="w-full max-w-md">
      <BarList data={sources} label="Traffic sources" />
    </div>
  );
}
```

## Second line

A `description` adds a quieter second line under the label, such as the path under a page title. It is set in 13 px text in the secondary text color, which stays readable on the bar in both themes, and its row and bar grow to hold it while the value stays on the first line. Like the label, it stays on one line: a text that is too long ends in an ellipsis, and its title attribute keeps the full text, which the browser shows on hover.

Top pages with their paths

[Complete TSX file](/examples/bar-list/second-line.tsx)

```tsx
"use client";

import { BarList } from "@/siteplane/components/bar-list";

const pages = [
  { description: "/pricing", id: "pricing", label: "Pricing", value: 8420 },
  {
    description: "/docs/installation",
    id: "installation",
    label: "Installation",
    value: 5160,
  },
  {
    description: "/blog/release-notes-for-the-october-update-and-every-change",
    id: "release-notes",
    label: "Release notes for the October update and every change in it",
    value: 2380,
  },
  {
    description: "/changelog",
    id: "changelog",
    label: "Changelog",
    value: 1240,
  },
  { description: "/about", id: "about", label: "About us", value: 615 },
];

export default function Example() {
  return (
    <div className="w-full max-w-md">
      <BarList data={pages} label="Top pages by visitors" />
    </div>
  );
}
```

## Heading

Pass `heading` to name the two columns in a row above the list: its `label` stands over the labels and its `value` over the values, and a [Separator](/docs/components/separator/) runs between the heading and the rows. The heading is text that screen readers read before the list.

Traffic sources under a heading row

[Complete TSX file](/examples/bar-list/heading.tsx)

```tsx
"use client";

import {
  GlobeIcon,
  LinkIcon,
  MailIcon,
  SearchIcon,
  Share2Icon,
} from "lucide-react";
import { BarList } from "@/siteplane/components/bar-list";

const sources = [
  { icon: <SearchIcon />, id: "search", label: "Search", value: 5210 },
  { icon: <GlobeIcon />, id: "direct", label: "Direct", value: 3480 },
  { icon: <Share2Icon />, id: "social", label: "Social", value: 1265 },
  { icon: <MailIcon />, id: "email", label: "Email", value: 840 },
  { icon: <LinkIcon />, id: "referral", label: "Referral", value: 415 },
];

export default function Example() {
  return (
    <div className="w-full max-w-md">
      <BarList
        data={sources}
        heading={{ label: "Source", value: "Visitors" }}
        label="Traffic sources"
      />
    </div>
  );
}
```

## Changing data

When `data` changes, the list glides to it. Each bar moves to its new length and each value rolls. A row that is new grows its bar out of nothing, a row that is gone shrinks away and leaves, and the rows that stay move to their new places. Rows are told by their `id`, so give every row one that is unique in the list and stays the same.

Top pages that glide when the period changes

[Complete TSX file](/examples/bar-list/changing.tsx)

```tsx
"use client";

import { useState } from "react";
import { BarList } from "@/siteplane/components/bar-list";
import {
  ToggleGroup,
  ToggleGroupItem,
} from "@/siteplane/components/toggle-group";

const periods = [
  {
    data: [
      { id: "pricing", label: "/pricing", value: 1920 },
      { id: "installation", label: "/docs/installation", value: 1340 },
      { id: "release-notes", label: "/blog/release-notes", value: 610 },
      { id: "changelog", label: "/changelog", value: 290 },
      { id: "about", label: "/about", value: 140 },
    ],
    label: "7 days",
    value: "7",
  },
  {
    data: [
      { id: "pricing", label: "/pricing", value: 8420 },
      { id: "installation", label: "/docs/installation", value: 5160 },
      { id: "release-notes", label: "/blog/release-notes", value: 2380 },
      { id: "changelog", label: "/changelog", value: 1240 },
      { id: "about", label: "/about", value: 615 },
      { id: "careers", label: "/careers", value: 48 },
    ],
    label: "30 days",
    value: "30",
  },
  {
    data: [
      { id: "installation", label: "/docs/installation", value: 16880 },
      { id: "pricing", label: "/pricing", value: 15930 },
      { id: "launch-week", label: "/blog/launch-week", value: 7210 },
      { id: "release-notes", label: "/blog/release-notes", value: 6040 },
      { id: "changelog", label: "/changelog", value: 3560 },
      { id: "about", label: "/about", value: 1790 },
    ],
    label: "90 days",
    value: "90",
  },
];

export default function Example() {
  const [value, setValue] = useState("30");
  const data = periods.find((period) => period.value === value)?.data ?? [];
  return (
    <div className="grid w-full max-w-md gap-4">
      <ToggleGroup
        aria-label="Period"
        onValueChange={([next]) => {
          if (next) setValue(next);
        }}
        size="sm"
        value={[value]}
        variant="outline"
      >
        {periods.map((period) => (
          <ToggleGroupItem key={period.value} value={period.value}>
            {period.label}
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
      <BarList data={data} label="Top pages by visitors" />
    </div>
  );
}
```

## Right-to-left

In right-to-left text the rows mirror: each bar starts on the right, the icon and the label sit on the right, and the values line up on the left. Set `dir="rtl"` on the page or on a container. Numbers still read from left to right.

Visitors by device, right to left

[Complete TSX file](/examples/bar-list/rtl.tsx)

```tsx
"use client";

import { MonitorIcon, SmartphoneIcon, TabletIcon } from "lucide-react";
import { BarList } from "@/siteplane/components/bar-list";

const devices = [
  { icon: <MonitorIcon />, id: "desktop", label: "Desktop", value: 6240 },
  { icon: <SmartphoneIcon />, id: "mobile", label: "Mobile", value: 4180 },
  { icon: <TabletIcon />, id: "tablet", label: "Tablet", value: 720 },
];

export default function Example() {
  return (
    <div className="w-full max-w-md" dir="rtl">
      <BarList data={devices} label="Visitors by device" />
    </div>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| BarList.data | readonly BarListItem[] | required | The rows, in the order they are shown. Sort the data to rank it. |
| BarList.label | string | required | Accessible name of the list. |
| BarList.heading | BarListHeading | unset | Names the two columns in a heading row above the rows, over a separator. |
| BarList.formatValue | (value: number) => string | Intl.NumberFormat(locale) | Writes the value at the end of each row. Text with digits rolls to its new value when it changes. |
| BarList.locale | string | en-US | Locale of the number format when formatValue is not given. |
| BarList.color | string | lightest brand shade | A CSS color or token for the bars. |
| BarList.emptyLabel | string | No data | Shown in place of the rows when data is empty. |
| BarList.className | string | unset | Additional classes for the list, or with a heading for the frame that holds the heading and the list. |
| BarListItem.id | string | required | Stable key of the row, unique in the list. Its bar glides by it when the data changes. |
| BarListItem.label | string | required | Text of the row, written over its bar. A label that does not fit on one line ends in an ellipsis, and its title attribute keeps the full text. |
| BarListItem.description | string | unset | A quieter second line under the label, such as a path. It ends in an ellipsis when it does not fit, with the full text in its title attribute. |
| BarListItem.value | number | required | Sets the length of the bar next to the largest value and is written at the end of the row. A value that is not a finite number counts as 0. |
| BarListItem.icon | ReactNode | unset | Shown before the label as decoration, hidden from assistive technology. An svg element takes the icon size of the kit. |
| BarListHeading.label | string | required | Name of the column of labels, such as Top pages, at the start of the heading row. |
| BarListHeading.value | string | required | Name of the column of values, such as Visitors, at the end of the heading row. |

BarListProps, BarListItem and BarListHeading export these contracts. The list
renders a `ul` with one `li` per row; with a `heading` it renders a `div` that
holds the heading row, the separator and the `ul`. It takes no native props or
ref beyond `className`.

[Read the installed source](/source/bar-list).

## Usage notes

Use a bar list for a short ranked list whose values should be seen next to the largest. When values can be negative or must be read against an axis, use a [Bar Chart](/docs/components/bar-chart/). For many rows or several values per row, use a [Table](/docs/components/table/).

Each bar is as long as its value is large next to the largest value, which fills the track. Any value above zero keeps at least 2% of the track, so a very small value is still a short bar and not a hairline. A value that is zero or negative has no bar, a negative value is still written as it is, and a value that is not a finite number counts as 0. All values share one column as wide as the widest, so every bar has the same track.

The rows are not interactive: they take no click, hover or focus, and they are not links. When a row must open or select something, use a Table, a [List Box](/docs/components/list-box/) or a [Resource List](/docs/components/resource-list/).

The bars take the lightest shade of the brand color. Each label is written over its bar, so a custom `color` should stay light enough to keep the labels readable. A label or a second line that does not fit on one line ends in an ellipsis, so every row keeps its height and the values stay in their column; the title attribute keeps the full text, and screen readers read it in full.

The list is named by `label` and is not a focus stop. Screen readers read each row’s label and value once; a rolling value is not announced while it changes. A row that is leaving is hidden from assistive technology.

The list renders completely on the server: each bar is a share of the track and needs no measuring. New data animates with the [Motion](/docs/components/motion/) tokens: bars and values with `--motion-duration-value`, rows that come, go or move with `--motion-duration-expand`, all with `--motion-ease-standard`. Under reduced motion new data shows at once: the system setting, `html` with `data-motion-reduce="true"` or `SiteplaneMotionProvider preset="reduced"`.
