Skip to content
UI Kit
Accent color

Bar chart for React

Columns and bars with rounded data ends, a 2 px surface gap, value labels where they fit, tooltips and arrow keys.

One of 7 components in Charts

4 variants

Compare categories at a glance

A bar chart compares amounts across categories, and the eye reads length more precisely than any other shape. This one draws columns and bars with rounded data ends and a small gap between stacked segments, puts value labels where they fit and moves through bars with the arrow keys.

Simple shows one value per category, the default for rankings and monthly totals. Grouped places two or three series side by side, for example plan A against plan B per month. Stacked shows the parts of each total, Horizontal turns the bars sideways for long category names such as product or country names.

Start the value axis at zero, a bar that starts at 80 lies about its length. Sort bars by value unless the categories have their own order such as months. Use the accent for the bar that matters and a quiet tone for the rest when you want to make one point.

Variants

Simple

One series, values on the caps, the busiest day keeps the accent.

Grouped

Two years side by side per quarter, the current one in the accent.

Stacked

Three channels stacked to the monthly total, separated by a thin surface gap.

Horizontal

Long category names on the left, bars sorted by size, the value at each tip.

Details

What Bar chart brings along, read from the registry and the source.

Category
Charts
Variants
4
Version
1.0.0 · since 1.0.0
File
components/ui/bar-chart.tsx
Brings along
utilskitchart
npm packages
None
Languages
Texts come through props
Studio
Adjust in code

Props

The BarChartProps interface from components/ui/bar-chart.tsx, with the comments from the code. Also takes every prop of ChartA11y.

  • datarequired
    Datum[]

    Rows, one per category, e.g. { month: '2026-03', online: 21400, store: 9800 }

  • xKeyrequired
    string

    Key of the category. "2026-03" and "2026-03-14" are read as dates and formatted per language.

  • seriesrequired
    ChartSeries[]

    One bar per series and category, in slot order (accent, grey, two accent tones)

  • formatoptional
    Intl.NumberFormatOptions

    Value format, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }

  • xFormatoptional
    Intl.DateTimeFormatOptions
  • tooltipXFormatoptional
    Intl.DateTimeFormatOptions
  • stackedoptional
    boolean

    Several series side by side (default) or stacked to a total

  • orientationoptional
    'vertical' | 'horizontal'

    vertical columns (default) or horizontal bars with the labels on the left

  • valueLabelsoptional
    boolean

    Value at the bar end. Single series and stacked totals, only where it fits.

  • highlightoptional
    string

    Single series: the category (x value) that keeps the accent, the others step back

  • heightoptional
    number

    Vertical: plot height in px, axis labels included

  • rowHeightoptional
    number

    Horizontal: height of one row in px

  • legendoptional
    boolean

    Legend above the plot. Default: for two series and more.

  • xLabeloptional
    string

    Header of the first table column

Accessibility

Read from the source of the component.

  • Needs no client state. The server writes it in full, so it reads without JavaScript too.

Motion

  • With reduced motion (prefers-reduced-motion) it stays still. 1 places in the code switch motion off then.
  • No animations, it stands still from the start.

Source

The file aiw add puts into your project. From then on it is yours.

Show the file, 144 lines
components/ui/bar-chart.tsx
'use client';

import { round } from '@/lib/kit';
import { CartesianFrame, barPath, numberFormatter, rampColor, seriesColor, seriesOpacity, textWidth, useIntlLocale, type ChartA11y, type ChartSeries, type Datum } from '@/components/ui/chart';

export interface BarChartProps extends ChartA11y {
  /** Rows, one per category, e.g. { month: '2026-03', online: 21400, store: 9800 } */
  data: Datum[];
  /** Key of the category. "2026-03" and "2026-03-14" are read as dates and formatted per language. */
  xKey: string;
  /** One bar per series and category, in slot order (accent, grey, two accent tones) */
  series: ChartSeries[];
  /** Value format, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 } */
  format?: Intl.NumberFormatOptions;
  xFormat?: Intl.DateTimeFormatOptions;
  tooltipXFormat?: Intl.DateTimeFormatOptions;
  /** Several series side by side (default) or stacked to a total */
  stacked?: boolean;
  /** vertical columns (default) or horizontal bars with the labels on the left */
  orientation?: 'vertical' | 'horizontal';
  /** Value at the bar end. Single series and stacked totals, only where it fits. */
  valueLabels?: boolean;
  /** Single series: the category (x value) that keeps the accent, the others step back */
  highlight?: string;
  /** Vertical: plot height in px, axis labels included */
  height?: number;
  /** Horizontal: height of one row in px */
  rowHeight?: number;
  /** Legend above the plot. Default: for two series and more. */
  legend?: boolean;
  /** Header of the first table column */
  xLabel?: string;
  className?: string;
}

const GAP = 2;
const MAX = 24;

/**
 * Columns or bars: simple, grouped, stacked and horizontal.
 *   <BarChart data={rows} xKey="month" series={[{ key: 'online', label: 'Online' }, { key: 'store', label: 'Store' }]} stacked />
 */
export function BarChart({ data, xKey, series, format, xFormat, tooltipXFormat, stacked = false, orientation = 'vertical', valueLabels, highlight, height = 260, rowHeight = 40, legend, xLabel, label, summary, labels, animate, className }: BarChartProps) {
  const intl = useIntlLocale();
  const fmt = numberFormatter(intl, format);
  const horizontal = orientation === 'horizontal';
  const num = (d: Datum, k: string) => (typeof d[k] === 'number' ? (d[k] as number) : 0);
  const totals = data.map((d) => series.reduce((a, s) => a + Math.max(0, num(d, s.key)), 0));
  const all = data.flatMap((d) => series.map((s) => num(d, s.key)));
  const extent: [number, number] = stacked ? [0, Math.max(0, ...totals)] : [Math.min(0, ...all), Math.max(0, ...all)];
  const single = series.length === 1;
  const tipValue = (i: number) => (stacked ? totals[i] : single ? num(data[i], series[0].key) : null);
  const labelW = valueLabels ? Math.max(0, ...data.map((_, i) => { const v = tipValue(i); return v === null ? 0 : textWidth(fmt(v), 12); })) + 10 : 0;
  const plotHeight = horizontal ? data.length * rowHeight + 30 : height;
  const dim = (i: number, si: number) => (highlight !== undefined && single && String(data[i][xKey]) !== highlight ? rampColor(1) : seriesColor(si, series[si].color));

  return (
    <CartesianFrame
      data={data}
      xKey={xKey}
      series={series}
      format={format}
      xFormat={xFormat}
      tooltipXFormat={tooltipXFormat}
      height={plotHeight}
      scale="band"
      horizontal={horizontal}
      extent={extent}
      legend={legend}
      legendShape="rect"
      padRight={horizontal ? labelW : 0}
      padTop={!horizontal && valueLabels ? 12 : 0}
      total={stacked}
      xLabel={xLabel}
      label={label}
      summary={summary}
      labels={labels}
      animate={animate}
      className={className}
      marks={({ cat, band, val, animated, n, active, focus }) => {
        const zero = val(0);
        const thick = stacked || single ? Math.min(MAX, band * (horizontal ? 0.56 : 0.6)) : Math.min(MAX, (band * 0.72 - GAP * (series.length - 1)) / series.length);
        const groupW = stacked || single ? thick : thick * series.length + GAP * (series.length - 1);
        const delay = (i: number) => (animated ? ({ '--kitch-d': `${round((i / Math.max(1, n)) * 420, 0)}ms` } as React.CSSProperties) : undefined);
        return data.map((d, i) => {
          const c = cat(i);
          const start = round(c - groupW / 2);
          const faded = active !== null && active !== i;
          let marks: React.ReactNode;
          if (stacked) {
            let acc = 0;
            const visible = series.map((s, si) => ({ s, si, v: Math.max(0, num(d, s.key)) })).filter((x) => x.v > 0);
            marks = visible.map(({ s, si, v }, k) => {
              const a = val(acc);
              acc += v;
              const b = val(acc);
              const isTop = k === visible.length - 1;
              // The 2px surface gap sits on top of every segment but the last
              const len = Math.abs(b - a) - (isTop ? 0 : GAP);
              if (len <= 0) return null;
              const path = horizontal ? barPath(a, start, len, thick, isTop ? 4 : 0, 'right') : barPath(start, b + (isTop ? 0 : GAP), thick, len, isTop ? 4 : 0, 'up');
              return <path key={s.key} d={path} fill={seriesColor(si, s.color)} className="kitch-series" opacity={seriesOpacity(si, focus)} />;
            });
          } else {
            marks = series.map((s, si) => {
              const v = num(d, s.key);
              const p = val(v);
              const off = round(start + si * (thick + GAP));
              const len = Math.abs(p - zero);
              if (len < 0.5) return null;
              const path = horizontal ? barPath(Math.min(p, zero), off, len, thick, 4, v >= 0 ? 'right' : 'left') : barPath(off, Math.min(p, zero), thick, len, 4, v >= 0 ? 'up' : 'down');
              return <path key={s.key} d={path} fill={dim(i, si)} className="kitch-series" opacity={seriesOpacity(si, focus)} />;
            });
          }
          const tip = tipValue(i);
          const text = valueLabels && tip !== null ? fmt(tip) : null;
          const fits = text && (horizontal || textWidth(text, 11) <= band - 2);
          const highlighted = highlight !== undefined && String(d[xKey]) === highlight;
          return (
            <g key={i} className="transition-opacity duration-200 motion-reduce:transition-none" opacity={faded ? 0.55 : 1}>
              <g className={animated ? (horizontal ? 'kitch-grow-x' : tip !== null && tip < 0 ? 'kitch-grow-y-down' : 'kitch-grow-y') : undefined} style={delay(i)}>
                {marks}
              </g>
              {fits && (
                <text
                  x={horizontal ? val(tip!) + 8 : c}
                  y={horizontal ? c : val(Math.max(0, tip!)) - 7}
                  dy={horizontal ? '0.34em' : undefined}
                  textAnchor={horizontal ? 'start' : 'middle'}
                  className={`tabular-nums ${horizontal ? 'text-[12px]' : 'text-[11px]'} ${animated ? 'kitch-rise' : ''}`}
                  fontWeight={highlighted ? 600 : 500}
                  fill={highlighted || horizontal ? 'var(--foreground)' : 'var(--muted-foreground)'}
                  style={animated ? ({ '--kitch-d': `${round(380 + (i / Math.max(1, n)) * 420, 0)}ms` } as React.CSSProperties) : undefined}
                >
                  {text}
                </text>
              )}
            </g>
          );
        });
      }}
    />
  );
}

Frequent questions

How do I install Bar chart?

With aiw add @ai-website/bar-chart. The command puts components/ui/bar-chart.tsx into your project, the code is yours from then on. It brings along chart. It needs no further npm packages.

Which variants does Bar chart have?

4 variants, Simple, Grouped, Stacked and Horizontal.

Can an AI assistant build with Bar chart?

Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Bar chart with aiw_components and install it with aiw_add @ai-website/bar-chart.

Often searched

Jump to

Copied to the clipboard