Skip to content
UI Kit
Accent color

Line chart for React

One or several lines over time with nice ticks, a crosshair tooltip, arrow key stepping and a data table for screen readers.

One of 7 components in Charts

4 variants

Trends over time, readable for everyone

A line chart shows how a value changes over time, and it is the chart people read fastest. This one is drawn in SVG by the kit, picks round tick values on its own, formats dates and numbers in the language of the page and shows a crosshair tooltip with every series at the hovered point.

Single series is the plain trend for one metric such as visitors or revenue. Several series compares up to four lines with a legend, This year and last year puts the current period against the previous one as a dashed line. Straight with dots draws straight segments with markers, honest for few data points such as quarterly figures.

Keep it to four lines or fewer, more turn into spaghetti. Label the last value at the end of each line when space allows. Arrow keys step through the points and a data table follows below for screen readers, keep both, they cost nothing and help everyone.

Variants

Single series

A smooth line in the accent, the last point marked and labelled.

Several series

Three sources in accent, grey and a light accent tone, with a legend and one tooltip for all.

This year and last year

The current year in the accent, last year as a quiet dashed grey line.

Straight with dots

Straight segments and a dot on every month, for counts where each point matters.

Details

What Line 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/line-chart.tsx
Brings along
utilskitchart
npm packages
None
Languages
Texts come through props
Studio
Adjust in code
In collections
App dashboard

Props

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

  • datarequired
    Datum[]

    Rows, one per x value, e.g. { month: '2026-01', revenue: 31200 }

  • xKeyrequired
    string

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

  • seriesrequired
    ChartSeries[]

    One line per series, in slot order (accent, grey, two accent tones)

  • formatoptional
    Intl.NumberFormatOptions

    Value format for tooltip, table and axis, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }

  • xFormatoptional
    Intl.DateTimeFormatOptions

    Axis format for dates, e.g. { month: 'short' }

  • tooltipXFormatoptional
    Intl.DateTimeFormatOptions

    Tooltip format for dates, e.g. { month: 'long', year: 'numeric' }

  • curveoptional
    'smooth' | 'linear'

    smooth (monotone, never overshoots) or straight segments

  • dotsoptional
    boolean | 'end'

    Dots on every point, only at the end, or none

  • endLabelsoptional
    boolean

    Value at the end of each line, next to the last point

  • baselineoptional
    'zero' | 'auto'

    zero: the axis starts at 0. auto: it hugs the data (good for small changes).

  • 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, e.g. "Month"

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

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

export interface LineChartProps extends ChartA11y {
  /** Rows, one per x value, e.g. { month: '2026-01', revenue: 31200 } */
  data: Datum[];
  /** Key of the x value. "2026-03" and "2026-03-14" are read as dates and formatted per language. */
  xKey: string;
  /** One line per series, in slot order (accent, grey, two accent tones) */
  series: ChartSeries[];
  /** Value format for tooltip, table and axis, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 } */
  format?: Intl.NumberFormatOptions;
  /** Axis format for dates, e.g. { month: 'short' } */
  xFormat?: Intl.DateTimeFormatOptions;
  /** Tooltip format for dates, e.g. { month: 'long', year: 'numeric' } */
  tooltipXFormat?: Intl.DateTimeFormatOptions;
  /** smooth (monotone, never overshoots) or straight segments */
  curve?: 'smooth' | 'linear';
  /** Dots on every point, only at the end, or none */
  dots?: boolean | 'end';
  /** Value at the end of each line, next to the last point */
  endLabels?: boolean;
  /** zero: the axis starts at 0. auto: it hugs the data (good for small changes). */
  baseline?: 'zero' | 'auto';
  /** 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, e.g. "Month" */
  xLabel?: string;
  className?: string;
}

/**
 * Lines over time or categories, one or several series.
 *   <LineChart data={rows} xKey="month" series={[{ key: 'revenue', label: 'Revenue' }]} format={{ style: 'currency', currency: 'EUR' }} />
 */
export function LineChart({ data, xKey, series, format, xFormat, tooltipXFormat, curve = 'smooth', dots = 'end', endLabels, baseline = 'zero', height = 260, legend, xLabel, label, summary, labels, animate, className }: LineChartProps) {
  const intl = useIntlLocale();
  const fmt = numberFormatter(intl, format);
  const nums = data.flatMap((d) => series.map((s) => d[s.key])).filter((v): v is number => typeof v === 'number');
  const lo = nums.length ? Math.min(...nums) : 0;
  const hi = nums.length ? Math.max(...nums) : 1;
  const extent: [number, number] = baseline === 'zero' ? [Math.min(0, lo), Math.max(0, hi)] : [lo - (hi - lo) * 0.08, hi + (hi - lo) * 0.08];
  const last = (key: string) => {
    for (let i = data.length - 1; i >= 0; i--) if (typeof data[i][key] === 'number') return { i, v: data[i][key] as number };
    return null;
  };
  const labelW = endLabels ? Math.max(...series.map((s) => textWidth(fmt(last(s.key)?.v ?? 0), 12))) + 14 : 0;
  // On narrow charts the end labels would squeeze the plot, the card header carries the value there
  const padRight = (w: number) => (w >= 480 ? labelW : 0);

  return (
    <CartesianFrame
      data={data}
      xKey={xKey}
      series={series}
      format={format}
      xFormat={xFormat}
      tooltipXFormat={tooltipXFormat}
      height={height}
      scale="point"
      extent={extent}
      legend={legend}
      legendShape="line"
      padRight={padRight}
      crosshair
      xLabel={xLabel}
      label={label}
      summary={summary}
      labels={labels}
      animate={animate}
      className={className}
      marks={({ cat, val, animated, width, focus }) =>
        series.map((s, si) => {
          const color = seriesColor(si, s.color);
          // Gaps (null) split the line into segments
          const segments: [number, number][][] = [[]];
          data.forEach((d, i) => {
            const v = d[s.key];
            if (typeof v === 'number') segments[segments.length - 1].push([cat(i), val(v)]);
            else if (segments[segments.length - 1].length) segments.push([]);
          });
          const end = last(s.key);
          return (
            <g key={s.key} className="kitch-series" opacity={seriesOpacity(si, focus)}>
              {segments
                .filter((seg) => seg.length)
                .map((seg, k) => (
                  <path
                    key={k}
                    d={linePath(seg, curve === 'smooth')}
                    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={animated ? ({ '--kitch-d': `${si * 120}ms` } as React.CSSProperties) : undefined}
                  />
                ))}
              {dots === true &&
                data.map((d, i) =>
                  typeof d[s.key] === 'number' ? (
                    <circle
                      key={i}
                      cx={cat(i)}
                      cy={val(d[s.key] as number)}
                      r="4"
                      fill={color}
                      stroke="var(--kitch-surface)"
                      strokeWidth="2"
                      className={animated ? 'kitch-pop' : undefined}
                      style={animated ? ({ '--kitch-d': `${round(120 + si * 120 + (i / Math.max(1, data.length - 1)) * 680, 0)}ms` } as React.CSSProperties) : undefined}
                    />
                  ) : null,
                )}
              {dots === 'end' && end && <circle cx={cat(end.i)} cy={val(end.v)} r="4" fill={color} stroke="var(--kitch-surface)" strokeWidth="2" className={animated ? 'kitch-pop' : undefined} style={animated ? ({ '--kitch-d': `${780 + si * 120}ms` } as React.CSSProperties) : undefined} />}
              {endLabels && end && width >= 480 && (
                <text x={cat(end.i) + 10} y={val(end.v)} dy="0.34em" className={`text-[12px] font-medium tabular-nums ${animated ? 'kitch-fade' : ''}`} fill="var(--foreground)" style={animated ? ({ '--kitch-d': `${860 + si * 120}ms` } as React.CSSProperties) : undefined}>
                  {fmt(end.v)}
                </text>
              )}
            </g>
          );
        })
      }
      overlay={({ cat, val, shown, open, jump, focus }) =>
        shown !== null &&
        series.map((s, si) => {
          const v = data[shown]?.[s.key];
          return typeof v === 'number' ? <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 Line chart?

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

4 variants, Single series, Several series, This year and last year and Straight with dots.

Can an AI assistant build with Line chart?

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

Often searched

Jump to

Copied to the clipboard