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
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.
Details
What Line chart brings along, read from the registry and the source.
- Category
- Charts
- Area
- Elements
- 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
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The LineChartProps interface from components/ui/line-chart.tsx, with the comments from the code. Also takes every prop of ChartA11y.
datarequiredDatum[]Rows, one per x value, e.g. { month: '2026-01', revenue: 31200 }
xKeyrequiredstringKey of the x value. "2026-03" and "2026-03-14" are read as dates and formatted per language.
seriesrequiredChartSeries[]One line per series, in slot order (accent, grey, two accent tones)
formatoptionalIntl.NumberFormatOptionsValue format for tooltip, table and axis, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }
xFormatoptionalIntl.DateTimeFormatOptionsAxis format for dates, e.g. { month: 'short' }
tooltipXFormatoptionalIntl.DateTimeFormatOptionsTooltip format for dates, e.g. { month: 'long', year: 'numeric' }
curveoptional'smooth' | 'linear'smooth (monotone, never overshoots) or straight segments
dotsoptionalboolean | 'end'Dots on every point, only at the end, or none
endLabelsoptionalbooleanValue 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).
heightoptionalnumberPlot height in px, axis labels included
legendoptionalbooleanLegend above the plot. Default: for two series and more.
xLabeloptionalstringHeader 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
'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.