# BarList

A ranked list, with the bar behind the name rather than beside it.

- Group: Data
- Import: `import { BarList } from '@misoto22/folio/charts'`
- Page: https://ui.misoto22.com/components/bar-list/
- Related: bar-chart, table

## When to reach for it

Top referrers, slowest endpoints, biggest accounts. A horizontal BarChart spends a third of its width on an axis repeating labels the rows could simply contain.

## Anatomy

- **Caption** (required) — label, as the table’s real caption — sr-only unless showLabel prints it as an eyebrow above the rows. It is what names the list for a screen reader.
- **Header row** (required) — An sr-only thead of two th scope="col" cells, Name and Value, so both columns are named even though the list never shows a header.
- **Name cell** (required) — A th scope="row" holding the row’s name truncated to one line, with item.icon before it as aria-hidden decoration and item.href turning the name itself into the link.
- **Bar** (required) — Not an element: a linear-gradient on the name cell’s inner span, a hard stop at the row’s share of the ceiling. Drawn as a sibling div it would be one more empty thing in the accessibility tree saying nothing.
- **Value cell** (required) — A td at the end edge in mono tabular figures at --ink-2, written by formatValue — the same compact default the axes use unless the call site replaces it.
- **Other row** — What limit adds: one final row named Other carrying the summed tail, so the rows shown still account for the whole they were cut from.
- **Empty state** — ChartEmpty under the label when there is nothing to rank. A caption over an empty tbody is a list that failed to load as far as the reader can tell, and reloading does not change it.

## Best practices

### Do

- Pin max when rows arrive over time. The ceiling is the largest row currently shown, so one new leader rescales every bar under it — a track that was two thirds full yesterday is a quarter full today with exactly the same number in it, and the movement the reader sees happened to a different row.
- Reach for limit rather than slicing items at the call site. The tail is summed into the Other row instead of being dropped, so the five rows shown still add up to the whole; a top five cut by hand discards the other forty silently, and nothing in the list says so.
- Keep names short enough to survive the truncation. The name is a single truncated line sharing its width with the value column, so two endpoints that differ only past the fortieth character render as the same row with the same ellipsis.
- Pass sort={false} when the order is the point — a funnel, a set of steps, a fixed set of regions. sort defaults to true and orders descending, which turns a sequence into a ranking without saying that it did.

### Don’t

- The bar is the row over the largest row, never a share of a total: the leading row always fills its track, so five rows that make up 3 percent of traffic look exactly like five that make up all of it. Where the whole matters, the caption is where it goes.
- Do not mix number shapes down the value column. It is end-aligned mono with tabular figures, which lines digits up only while the strings are the same shape — and the default formatter switches to the compact form at 10,000, so a list spanning that threshold puts 9,400 under 1.2M and there is nothing left to compare down.
- Two rows cannot share a name. It is the row’s label and its React key at once, so a list built from a query that can repeat a label renders duplicate keys, which React warns about and reconciles wrongly the moment the list updates.

## Accessibility

- A real <table> with two columns and one row per thing, because that is what a ranked list is. The bar is a background on the name cell, so it is never a second element a screen reader has to walk past.
- limit sums the tail into an “Other” row rather than dropping it — a top five that silently discards the other forty misstates the whole, and the reader has no way to tell.
- Pin max to compare two lists side by side: on independent scales the leading row of each fills its track, and two very different numbers look identical.

## BarList

A ranked list, with the bar behind the name rather than beside it. The answer to "top referrers", "slowest endpoints", "biggest accounts" — and a better one than a horizontal bar chart, which spends a third of its width on a category axis repeating labels the rows could simply contain. Reading a name off a y-axis and matching it to a bar is two steps; reading it off the bar is none. It is a `<table>`, because that is what it is: two columns, a header, and one row per thing. The bar is a background on the name cell, so it never becomes a second element a screen reader has to walk past. Reach for `<BarChart orientation="horizontal">` instead when the categories are few and fixed and the axis is doing real work — a scale a reader needs to read values off, rather than a ranking they need to skim.

### Props

- `label` (required) — `string`. What the list ranks. Required: it names the table for a screen reader.
- `showLabel` — `boolean` default `false`. Prints the label above the list instead of hiding it from sight.
- `items` (required) — `BarListItem[]`.
- `limit` — `number`. Keeps the top N and folds the rest into one "Other" row.
- `formatValue` — `(value: number) => string` default `defaultTick`. Formats each value. Defaults to the same compact form the axes use.
- `max` — `number`. The scale's ceiling. Derived from the largest row when omitted. Pin it to compare two lists side by side — on independent scales the leading row of each fills its track, and two very different numbers look identical.
- `sort` — `boolean` default `true`. Sorts descending before rendering.
- `className` — `string`.
- `empty` — `ChartEmptyProps`. What the list shows when it has nothing to rank. A caption over an empty `<tbody>` is a list that failed to load as far as the reader can tell, and the reader's next move is to reload the page.

## Example — default

```tsx
'use client'

import { BarList } from '@misoto22/folio/charts'

const referrers = [
  { name: 'google.com', value: 42_100 },
  { name: 'github.com', value: 18_800 },
  { name: 'news.ycombinator.com', value: 9_400 },
  { name: 'x.com', value: 6_120 },
  { name: 'reddit.com', value: 3_280 },
  { name: 'lobste.rs', value: 1_940 },
  { name: 'bsky.app', value: 1_100 },
  { name: 'linkedin.com', value: 640 },
]

export function Example() {
  return <BarList label="Top referrers" showLabel items={referrers} />
}
```

## Example — limit

```tsx
'use client'

import { BarList } from '@misoto22/folio/charts'

const referrers = [
  { name: 'google.com', value: 42_100, href: 'https://google.com' },
  { name: 'github.com', value: 18_800, href: 'https://github.com' },
  { name: 'news.ycombinator.com', value: 9_400, href: 'https://news.ycombinator.com' },
  { name: 'x.com', value: 6_120 },
  { name: 'reddit.com', value: 3_280 },
  { name: 'lobste.rs', value: 1_940 },
  { name: 'bsky.app', value: 1_100 },
  { name: 'linkedin.com', value: 640 },
]

export function Example() {
  return <BarList label="Top referrers" showLabel items={referrers} limit={3} />
}
```

## Example — a shared ceiling

```tsx
import { BarList, formatNumber } from '@misoto22/folio/charts'

<div className="grid w-full gap-8 sm:grid-cols-2">
  <BarList
    label="New ARR this quarter"
    showLabel
    items={thisQuarter}
    max={CEILING}
    formatValue={money}
  />
  <BarList
    label="New ARR last quarter"
    showLabel
    items={lastQuarter}
    max={CEILING}
    formatValue={money}
  />
</div>
```
