Skip to content
    Style: Wireframe

    Components · Data

    Bar chart

    Columns with their values on top and labels under them; the same data in a table for screen readers.

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

    Deploys per month235 in six months
    Deploys per month
    LabelValue
    Jun24 deploys
    Jul31 deploys
    Aug18 deploys
    Sep42 deploys
    Oct57 deploys
    Nov63 deploys

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/bar-chart.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/bar-chart.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS.

    bar-chart.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
    /** 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))}%`;