# Bar chart (Caseroom component)

Columns with their values on top and labels under them; the same data in a table for screen readers. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

0.92% of the 2,062 sites in The UI Index have a chart (19 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/bar-chart.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/bar-chart.json`
- HTML: https://www.themes.dev/caseroom/r/html/bar-chart.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/bar-chart/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### bar-chart.astro

```astro
---
import { cn } from '@/lib/utils';
import { barChart as c, heightOf, type Bar } from './bar-chart.variants';

interface Props {
  title: string;
  data: Bar[];
  /** Shown after numbers, e.g. " deploys" or "%". */
  unit?: string;
  /** A line at the top right, e.g. "Total 412". */
  summary?: string;
  class?: string;
}
const { title, data, unit = '', summary, class: className } = Astro.props;
const max = Math.max(...data.map((d) => d.value));
---

<figure class={cn(c.root, className)} style={`--bars: ${data.length}`}>
  <figcaption class={c.head}>
    <span class={c.title}>{title}</span>
    {summary && <span class={c.total}>{summary}</span>}
  </figcaption>
  <div aria-hidden="true">
    <div class={c.plot}>
      {data.map((d) => (
        <div class={c.col}>
          <span class={c.bar} style={`height: ${heightOf(d.value, max)}`}><span class={c.value}>{d.value}</span></span>
        </div>
      ))}
    </div>
    <div class={c.labels}>{data.map((d) => <span class={c.label}>{d.label}</span>)}</div>
  </div>
  <table class="sr-only">
    <caption>{title}</caption>
    <thead><tr><th scope="col">Label</th><th scope="col">Value</th></tr></thead>
    <tbody>{data.map((d) => <tr><th scope="row">{d.label}</th><td>{d.value}{unit}</td></tr>)}</tbody>
  </table>
</figure>
```

### bar-chart.tsx

```tsx
import type { CSSProperties } from 'react';
import { cn } from '@/lib/utils';
import { barChart as c, heightOf, type Bar } from './bar-chart.variants';

export type { Bar };

export interface BarChartProps {
  title: string;
  data: Bar[];
  /** Shown after numbers, e.g. " deploys" or "%". */
  unit?: string;
  /** A line at the top right, e.g. "Total 412". */
  summary?: string;
  className?: string;
}

/** Columns with their values on top, labels under them, and the same data in a table for screen readers. */
export function BarChart({ title, data, unit = '', summary, className }: BarChartProps) {
  const max = Math.max(...data.map((d) => d.value));
  return (
    <figure className={cn(c.root, className)} style={{ '--bars': data.length } as CSSProperties}>
      <figcaption className={c.head}>
        <span className={c.title}>{title}</span>
        {summary && <span className={c.total}>{summary}</span>}
      </figcaption>
      <div aria-hidden="true">
        <div className={c.plot}>
          {data.map((d) => (
            <div key={d.label} className={c.col}>
              <span className={c.bar} style={{ height: heightOf(d.value, max) }}><span className={c.value}>{d.value}</span></span>
            </div>
          ))}
        </div>
        <div className={c.labels}>{data.map((d) => <span key={d.label} className={c.label}>{d.label}</span>)}</div>
      </div>
      <table className="sr-only">
        <caption>{title}</caption>
        <thead><tr><th scope="col">Label</th><th scope="col">Value</th></tr></thead>
        <tbody>{data.map((d) => <tr key={d.label}><th scope="row">{d.label}</th><td>{d.value}{unit}</td></tr>)}</tbody>
      </table>
    </figure>
  );
}
```

### bar-chart.variants.ts

```tsx
/** A bar chart in HTML and CSS: columns grow from a baseline to their share of the largest value, each with its
 *  value on top and its label under it, so the numbers are read, not guessed. Ink when an accent is on (primary),
 *  otherwise black. The same data is in a table for screen readers; the drawing is hidden from them. The columns
 *  rise once on load; under reduced motion they stand still. */
export const barChart = {
  root: 'w-full',
  head: 'flex items-baseline justify-between gap-4',
  title: 'text-sm font-medium',
  total: 'text-sm text-muted-foreground tabular-nums',
  // The value sits above its bar (absolute), so a bar's height is its share of the whole column.
  plot: 'mt-6 grid h-48 items-end gap-2 border-b pt-6 [grid-template-columns:repeat(var(--bars),minmax(0,1fr))]',
  col: 'group flex h-full flex-col items-center justify-end',
  value: 'absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground',
  bar: 'relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none',
  labels: 'mt-2 grid gap-2 [grid-template-columns:repeat(var(--bars),minmax(0,1fr))]',
  label: 'truncate text-center text-xs text-muted-foreground',
};
export type Bar = { label: string; value: number };
export const heightOf = (value: number, max: number) => `${Math.max(2, Math.round((value / (max || 1)) * 100))}%`;
```
