Skip to content
UI Kit
Accent color

Area chart

A line with a quiet tint below, as a wash, a gradient or stacked bands with a surface gap.

One of 7 components in Charts

3 variants

Variants

Simple

Daily values with an even wash of the accent, weekends stand out by themselves.

Gradient

The tint fades towards the baseline, the line stays crisp on top.

Stacked

Three channels stacked to the weekly total, the tooltip adds them up.

Details

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

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

Props

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

  • datarequired
    Datum[]

    Rows, one per x value, e.g. { day: '2026-09-14', visits: 412 }

  • xKeyrequired
    string

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

  • seriesrequired
    ChartSeries[]

    One area per series, 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

    Stack the series to a total. Each band gets a tint and a top line in its series color.

  • filloptional
    'wash' | 'gradient'

    wash: an even, quiet tint. gradient: the tint fades towards the baseline.

  • curveoptional
    'smooth' | 'linear'
  • heightoptional
    number

    Plot height in px, axis labels included

  • 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

  • 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, 134 lines
components/ui/area-chart.tsx
'use client';

import { useId } from 'react';
import { CartesianFrame, GlideDot, linePath, seriesColor, seriesOpacity, type ChartA11y, type ChartSeries, type Datum } from '@/components/ui/chart';

export interface AreaChartProps extends ChartA11y {
  /** Rows, one per x value, e.g. { day: '2026-09-14', visits: 412 } */
  data: Datum[];
  /** Key of the x value. "2026-03" and "2026-03-14" are read as dates and formatted per language. */
  xKey: string;
  /** One area per series, 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;
  /** Stack the series to a total. Each band gets a tint and a top line in its series color. */
  stacked?: boolean;
  /** wash: an even, quiet tint. gradient: the tint fades towards the baseline. */
  fill?: 'wash' | 'gradient';
  curve?: 'smooth' | 'linear';
  /** Plot height in px, axis labels included */
  height?: number;
  /** Legend above the plot. Default: for two series and more. */
  legend?: boolean;
  /** Header of the first table column */
  xLabel?: string;
  className?: string;
}

/**
 * Areas over time: a single tinted area, a gradient, or stacked bands.
 *   <AreaChart data={rows} xKey="day" series={[{ key: 'visits', label: 'Visits' }]} fill="gradient" />
 */
export function AreaChart({ data, xKey, series, format, xFormat, tooltipXFormat, stacked = false, fill = 'wash', curve = 'smooth', height = 260, legend, xLabel, label, summary, labels, animate, className }: AreaChartProps) {
  const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
  const num = (d: Datum, k: string) => (typeof d[k] === 'number' ? (d[k] as number) : null);
  // Cumulative tops per series when stacked
  const tops = series.map((_, si) => data.map((d) => series.slice(0, si + 1).reduce((a, s) => a + Math.max(0, num(d, s.key) ?? 0), 0)));
  const all = stacked ? tops[tops.length - 1] ?? [] : data.flatMap((d) => series.map((s) => num(d, s.key))).filter((v): v is number => v !== null);
  const extent: [number, number] = [Math.min(0, ...all), Math.max(0, ...all)];
  const smooth = curve === 'smooth';

  return (
    <CartesianFrame
      data={data}
      xKey={xKey}
      series={series}
      format={format}
      xFormat={xFormat}
      tooltipXFormat={tooltipXFormat}
      height={height}
      scale="point"
      extent={extent}
      legend={legend}
      legendShape={stacked ? 'rect' : 'line'}
      crosshair
      total={stacked}
      xLabel={xLabel}
      label={label}
      summary={summary}
      labels={labels}
      animate={animate}
      className={className}
      marks={({ cat, val, animated, focus }) => (
        <>
          <defs>
            {series.map((s, si) => (
              <linearGradient key={s.key} id={`${uid}-g${si}`} x1="0" x2="0" y1="0" y2="1">
                <stop offset="0" stopColor={seriesColor(si, s.color)} stopOpacity={stacked ? 0.55 : 0.3} />
                <stop offset="1" stopColor={seriesColor(si, s.color)} stopOpacity={stacked ? 0.22 : 0} />
              </linearGradient>
            ))}
          </defs>
          {/* Draw from the last series down, so the first sits in front when not stacked */}
          {[...series.keys()].reverse().map((si) => {
            const s = series[si];
            const color = seriesColor(si, s.color);
            const style = animated ? ({ '--kitch-d': `${si * 120}ms` } as React.CSSProperties) : undefined;
            const op = seriesOpacity(si, focus);
            if (stacked) {
              const upper: [number, number][] = data.map((_, i) => [cat(i), val(tops[si][i])]);
              const lower: [number, number][] = data.map((_, i) => [cat(i), val(si ? tops[si - 1][i] : 0)]).reverse() as [number, number][];
              const top = linePath(upper, smooth);
              const bottom = linePath(lower, smooth).replace(/^M/, 'L');
              return (
                <g key={s.key} className="kitch-series" opacity={op}>
                  <path d={`${top} ${bottom} Z`} fill={fill === 'gradient' ? `url(#${uid}-g${si})` : color} fillOpacity={fill === 'gradient' ? 1 : 0.4} className={animated ? 'kitch-clip' : undefined} style={style} />
                  {/* Each band ends in a crisp line of its own color */}
                  <path d={top} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" pathLength={1} className={animated ? 'kitch-draw' : undefined} style={style} />
                </g>
              );
            }
            const segments: [number, number][][] = [[]];
            data.forEach((d, i) => {
              const v = num(d, s.key);
              if (v !== null) segments[segments.length - 1].push([cat(i), val(v)]);
              else if (segments[segments.length - 1].length) segments.push([]);
            });
            return (
              <g key={s.key} className="kitch-series" opacity={op}>
                {segments
                  .filter((seg) => seg.length > 1)
                  .map((seg, k) => {
                    const top = linePath(seg, smooth);
                    const base = val(Math.max(extent[0], 0));
                    return (
                      <g key={k}>
                        <path
                          d={`${top} L${seg[seg.length - 1][0]} ${base} L${seg[0][0]} ${base} Z`}
                          fill={fill === 'gradient' ? `url(#${uid}-g${si})` : color}
                          fillOpacity={fill === 'gradient' ? 1 : si === 0 ? 0.12 : 0.1}
                          className={animated ? 'kitch-clip' : undefined}
                          style={style}
                        />
                        <path d={top} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={s.dashed ? '4 5' : undefined} pathLength={s.dashed ? undefined : 1} className={animated ? (s.dashed ? 'kitch-clip' : 'kitch-draw') : undefined} style={style} />
                      </g>
                    );
                  })}
              </g>
            );
          })}
        </>
      )}
      overlay={({ cat, val, shown, open, jump, focus }) =>
        shown !== null &&
        series.map((s, si) => {
          const v = stacked ? tops[si][shown] : num(data[shown], s.key);
          return v !== null && v !== undefined ? <GlideDot key={s.key} x={cat(shown)} y={val(v)} open={open} jump={jump} color={seriesColor(si, s.color)} opacity={seriesOpacity(si, focus)} /> : null;
        })
      }
    />
  );
}

Frequent questions

How do I install Area chart?

With aiw add @ai-website/area-chart. The command puts components/ui/area-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 Area chart have?

3 variants, Simple, Gradient and Stacked.

Can an AI assistant build with Area chart?

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

Often searched

Jump to

Copied to the clipboard