One or two series drawn on the server, with an arrow-key readout and a table view.
One series
A line chart shows a value across a run of days as a 2px ink line. Gridlines sit at the yTicks, with their labels above them, and the first, middle and last day are labeled underneath. Focus or point at the plot to see a readout of the day and its value.
Example
Example
| This month | |
|---|---|
| Oct 1 | $42.10 |
| Oct 2 | $38.50 |
| Oct 3 | $55.00 |
| Oct 4 | $61.20 |
| Oct 5 | $47.80 |
| Oct 6 | $33.40 |
| Oct 7 | $29.90 |
| Oct 8 | $52.60 |
| Oct 9 | $64.30 |
| Oct 10 | $58.00 |
| Oct 11 | $49.50 |
| Oct 12 | $71.20 |
| Oct 13 | $66.80 |
| Oct 14 | $54.10 |
| Oct 15 | $45.70 |
| Oct 16 | $39.20 |
| Oct 17 | $62.40 |
| Oct 18 | $77.90 |
| Oct 19 | $70.30 |
| Oct 20 | $58.80 |
| Oct 21 | $51.60 |
| Oct 22 | $48.20 |
| Oct 23 | $66.00 |
| Oct 24 | $73.50 |
| Oct 25 | $80.10 |
| Oct 26 | $69.40 |
| Oct 27 | $57.30 |
| Oct 28 | $62.90 |
| Oct 29 | $75.60 |
| Oct 30 | $81.80 |
| Oct 31 | $84.20 |
import { LineChart } from '@snapshot-ui/charts';
import { Card } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
import { DAYS, THIS_MONTH, TICKS } from './line-chart-data';
export default function LineChartBasic(): ReactElement {
return (
<div className="chart-sample">
<Card>
<LineChart aria-label="Spending in October" series={[THIS_MONTH]} xLabels={DAYS} yTicks={TICKS} />
</Card>
</div>
);
}Compared with last month
A second series is context, not a second story. It is drawn thinner, in slate, under the first, and a legend names both. The readout and the table name each series beside its value, so color is never the only difference.
Example
Example
- This month
- Last month
| This month | Last month | |
|---|---|---|
| Oct 1 | $42.10 | $35.00 |
| Oct 2 | $38.50 | $41.20 |
| Oct 3 | $55.00 | $47.50 |
| Oct 4 | $61.20 | $44.80 |
| Oct 5 | $47.80 | $39.10 |
| Oct 6 | $33.40 | $36.60 |
| Oct 7 | $29.90 | $42.30 |
| Oct 8 | $52.60 | $50.40 |
| Oct 9 | $64.30 | $53.70 |
| Oct 10 | $58.00 | $49.90 |
| Oct 11 | $49.50 | $46.20 |
| Oct 12 | $71.20 | $52.80 |
| Oct 13 | $66.80 | $58.10 |
| Oct 14 | $54.10 | $55.60 |
| Oct 15 | $45.70 | $51.30 |
| Oct 16 | $39.20 | $47.00 |
| Oct 17 | $62.40 | $53.90 |
| Oct 18 | $77.90 | $60.20 |
| Oct 19 | $70.30 | $57.40 |
| Oct 20 | $58.80 | $54.00 |
| Oct 21 | $51.60 | $50.70 |
| Oct 22 | $48.20 | $56.30 |
| Oct 23 | $66.00 | $61.80 |
| Oct 24 | $73.50 | $59.50 |
| Oct 25 | $80.10 | $63.20 |
| Oct 26 | $69.40 | $60.00 |
| Oct 27 | $57.30 | $56.80 |
| Oct 28 | $62.90 | $62.50 |
| Oct 29 | $75.60 | $66.10 |
| Oct 30 | $81.80 | $64.40 |
| Oct 31 | $84.20 | $61.00 |
import { LineChart } from '@snapshot-ui/charts';
import { Card } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
import { DAYS, LAST_MONTH, THIS_MONTH, TICKS } from './line-chart-data';
export default function LineChartCompare(): ReactElement {
return (
<div className="chart-sample">
<Card>
<LineChart
aria-label="Spending in October, against September"
series={[THIS_MONTH, LAST_MONTH]}
xLabels={DAYS}
yTicks={TICKS}
/>
</Card>
</div>
);
}Gaps
Pass null for a day with no value, such as one still to come. The line breaks there, the keys and pointer skip it, the readout leaves out that series, and the table cell is empty. hasArea fills under the first series with a 10% ink wash, which breaks with the line.
Example
Example
| This month | |
|---|---|
| Oct 1 | $42.10 |
| Oct 2 | $38.50 |
| Oct 3 | $55.00 |
| Oct 4 | $61.20 |
| Oct 5 | $47.80 |
| Oct 6 | $33.40 |
| Oct 7 | $29.90 |
| Oct 8 | $52.60 |
| Oct 9 | $64.30 |
| Oct 10 | $58.00 |
| Oct 11 | $49.50 |
| Oct 12 | $71.20 |
| Oct 13 | $66.80 |
| Oct 14 | $54.10 |
| Oct 15 | $45.70 |
| Oct 16 | $39.20 |
| Oct 17 | $62.40 |
| Oct 18 | $77.90 |
| Oct 19 | |
| Oct 20 | |
| Oct 21 | |
| Oct 22 | |
| Oct 23 | |
| Oct 24 | |
| Oct 25 | |
| Oct 26 | |
| Oct 27 | |
| Oct 28 | |
| Oct 29 | |
| Oct 30 | |
| Oct 31 |
import { LineChart, type LineSeries } from '@snapshot-ui/charts';
import { Card } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
import { DAYS, THIS_MONTH, TICKS } from './line-chart-data';
/** This month on Oct 18: the days still to come have no value, so they are null. */
const SO_FAR: LineSeries = {
label: 'This month',
points: THIS_MONTH.points.map((point, index) => (index < 18 ? point : null)),
};
export default function LineChartGaps(): ReactElement {
return (
<div className="chart-sample">
<Card>
<LineChart
aria-label="Spending so far in October"
series={[SO_FAR]}
xLabels={DAYS}
yTicks={TICKS}
hasArea
/>
</Card>
</div>
);
}The table
The Show as table toggle under every chart swaps the plot for a table of the same strings, with a row per day and a column per series. It is visible to everyone who would rather read the numbers, not only screen reader users. With scripts off, the table is shown under the plot with a note saying so.
The table is named like the chart. With aria-label, a caption repeats it. With aria-labelledby, the table points at the same heading and has no caption.
Right to left
Time follows the page's direction. Under dir="rtl" the newest day is at the left, and Left moves to newer days. The svg, its marks and the keys all read the nearest dir="ltr" or dir="rtl" attribute, so they always agree; dir="auto" is skipped. The figures inside the labels are your strings.
Example
Example
- This month
- Last month
| This month | Last month | |
|---|---|---|
| Oct 1 | $42.10 | $35.00 |
| Oct 2 | $38.50 | $41.20 |
| Oct 3 | $55.00 | $47.50 |
| Oct 4 | $61.20 | $44.80 |
| Oct 5 | $47.80 | $39.10 |
| Oct 6 | $33.40 | $36.60 |
| Oct 7 | $29.90 | $42.30 |
| Oct 8 | $52.60 | $50.40 |
| Oct 9 | $64.30 | $53.70 |
| Oct 10 | $58.00 | $49.90 |
| Oct 11 | $49.50 | $46.20 |
| Oct 12 | $71.20 | $52.80 |
| Oct 13 | $66.80 | $58.10 |
| Oct 14 | $54.10 | $55.60 |
| Oct 15 | $45.70 | $51.30 |
| Oct 16 | $39.20 | $47.00 |
| Oct 17 | $62.40 | $53.90 |
| Oct 18 | $77.90 | $60.20 |
| Oct 19 | $70.30 | $57.40 |
| Oct 20 | $58.80 | $54.00 |
| Oct 21 | $51.60 | $50.70 |
| Oct 22 | $48.20 | $56.30 |
| Oct 23 | $66.00 | $61.80 |
| Oct 24 | $73.50 | $59.50 |
| Oct 25 | $80.10 | $63.20 |
| Oct 26 | $69.40 | $60.00 |
| Oct 27 | $57.30 | $56.80 |
| Oct 28 | $62.90 | $62.50 |
| Oct 29 | $75.60 | $66.10 |
| Oct 30 | $81.80 | $64.40 |
| Oct 31 | $84.20 | $61.00 |
import { LineChart } from '@snapshot-ui/charts';
import { Card } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
import { DAYS, LAST_MONTH, THIS_MONTH, TICKS } from './line-chart-data';
export default function LineChartRtl(): ReactElement {
return (
<div className="chart-sample" dir="rtl">
<Card>
<LineChart
aria-label="Spending in October, right to left"
series={[THIS_MONTH, LAST_MONTH]}
xLabels={DAYS}
yTicks={TICKS}
/>
</Card>
</div>
);
}States
status draws a state in the plot's own space, at its height, as a bar chart does, so the page does not move when the data arrives. The gridlines' labels and the day labels you pass stay.
- Loading: a still placeholder in Skeleton's fill where the line will be, with no legend, toggle or table. The chart says "Loading" once, politely.
- Empty: a sentence in slate in the plot, or your
statusContent, such as an EmptyState. No table toggle. - Error: a sentence in the error color, which means a load failed, or your
statusContentwith a way to try again. The chart says it once.
The plot is no tab stop while it shows a state, and its keys do nothing; focus that was on it stays there. The three strings are statusLabels, a prop of their own with English defaults, so the four labels you already pass are unchanged.
Example
Example
No spending yet
November's spending shows here once a transaction arrives.
| This month |
|---|
The chart could not be loaded
Check your connection, then try again.
| This month | |
|---|---|
| Oct 1 | $42.10 |
| Oct 2 | $38.50 |
| Oct 3 | $55.00 |
| Oct 4 | $61.20 |
| Oct 5 | $47.80 |
| Oct 6 | $33.40 |
| Oct 7 | $29.90 |
| Oct 8 | $52.60 |
| Oct 9 | $64.30 |
| Oct 10 | $58.00 |
| Oct 11 | $49.50 |
| Oct 12 | $71.20 |
| Oct 13 | $66.80 |
| Oct 14 | $54.10 |
| Oct 15 | $45.70 |
| Oct 16 | $39.20 |
| Oct 17 | $62.40 |
| Oct 18 | $77.90 |
| Oct 19 | $70.30 |
| Oct 20 | $58.80 |
| Oct 21 | $51.60 |
| Oct 22 | $48.20 |
| Oct 23 | $66.00 |
| Oct 24 | $73.50 |
| Oct 25 | $80.10 |
| Oct 26 | $69.40 |
| Oct 27 | $57.30 |
| Oct 28 | $62.90 |
| Oct 29 | $75.60 |
| Oct 30 | $81.80 |
| Oct 31 | $84.20 |
'use client';
import { LineChart } from '@snapshot-ui/charts';
import { Button, Card, EmptyState } from '@snapshot-ui/react';
import { ChartLine, CloudOff } from 'lucide-react';
import { useState, type ReactElement } from 'react';
import { DAYS, THIS_MONTH, TICKS } from './line-chart-data';
export default function LineChartStates(): ReactElement {
// A failed load the reader can retry: Try again draws the chart, as a fetch that succeeded would.
const [hasFailed, setFailed] = useState(true);
return (
<div className="chart-sample stack">
<Card>
<LineChart
aria-label="Spending in October, loading"
series={[THIS_MONTH]}
xLabels={DAYS}
yTicks={TICKS}
status="loading"
/>
</Card>
<Card>
<LineChart
aria-label="Spending in November"
series={[{ label: 'This month', points: [] }]}
xLabels={[]}
status="empty"
statusContent={
<EmptyState
headingLevel={3}
icon={<ChartLine />}
title="No spending yet"
description="November's spending shows here once a transaction arrives."
/>
}
/>
</Card>
<Card>
<LineChart
aria-label="Spending in October, after a retry"
series={[THIS_MONTH]}
xLabels={DAYS}
yTicks={TICKS}
status={hasFailed ? 'error' : 'ready'}
statusContent={
<EmptyState
headingLevel={3}
icon={<CloudOff />}
title="The chart could not be loaded"
description="Check your connection, then try again."
action={
<Button
variant="secondary"
onPress={() => {
setFailed(false);
}}
>
Try again
</Button>
}
/>
}
/>
</Card>
</div>
);
}Usage
pnpm add @snapshot-ui/charts @snapshot-ui/coreIt needs the same 0.x minor of @snapshot-ui/core (0.10 for charts 0.2.1), whose styles.css styles it. The svg, axis labels, legend and table render on the server, and the client adds the keys, pointer and readout. A server component can render it with plain data.
- Formatting: every label and value is your formatted string.
valueLabelis what the reader sees, andvalueonly places the point. Currency, compact figures and Persian digits are yours, and the server's HTML is final. yTicks: gridlines with labels. Without them, the chart draws only its baseline and relies on the readout and the table.yDomain: pass the same one to charts read side by side, so their scales match.labels: in another language, pass all four strings the chart writes itself: the toggle's two, the note shown with scripts off, and the role description a screen reader says in place of "chart".statusLabels: and all three of its states' strings, so no English is left on the page while it loads, is empty or fails.
import { LineChart } from '@snapshot-ui/charts';
<LineChart
aria-label="هزینههای مهر"
series={[
{ label: 'این ماه', points: [{ value: 42.1, valueLabel: '۴۲٫۱۰ دلار' }, null] },
{ label: 'ماه گذشته', points: [{ value: 35, valueLabel: '۳۵ دلار' }, { value: 41.2, valueLabel: '۴۱٫۲۰ دلار' }] },
]}
xLabels={['۱ مهر', '۲ مهر']}
yTicks={[
{ value: 0, label: '۰ دلار' },
{ value: 50, label: '۵۰ دلار' },
]}
labels={{
showTable: 'نمایش جدول',
showChart: 'نمایش نمودار',
noScript: 'اسکریپتها خاموشاند، پس دادههای نمودار در جدول زیر است.',
roleDescription: 'نمودار',
}}
/>;Props
A LineChart accepts every HTML attribute, applied to its root figure.
aria-label string | A name for assistive technology. Give it, or |
|---|---|
aria-labelledby string | The id of an element that holds the name, such as a label shown elsewhere. |
className string | |
hasArea boolean | Fills under the first series. Default: false |
height number | Height of the plot in px. Default: 240 |
labels LineChartLabels | Default: { showTable: 'Show as table', showChart: 'Show as chart', noScript: "Scripts are off, so the chart's data is in the table below.", roleDescription: 'chart' } |
series readonly [LineSeries] | readonly [LineSeries, LineSeries] | One or two series. The first is the story, drawn in ink; the second is context, drawn in slate. |
status ChartStatus | Loading, empty or failed, drawn in the plot's own space at its height. Default: 'ready' |
statusContent ReactNode | Drawn in the plot's place for 'empty' and 'error', such as an EmptyState with a Retry button. |
statusLabels ChartStatusLabels | The three states' strings, apart from Default: { loading: 'Loading', empty: 'Nothing to show for this period', error: 'The chart could not be loaded' } |
xLabels readonly string[] | The x positions' labels, formatted: "Oct 1", "Oct 2". |
xTicks readonly number[] | Which x labels the axis shows, by index. Default: first, middle and last |
yDomain readonly [number, number] | The y domain. Default: from zero to the nice ceiling of the data |
yTicks readonly { value: number; label: string; }[] | The y gridlines and their labels, formatted. When absent the chart draws no y labels and relies on the readout and the table. |
label string | "This month", "Last month". Required: it names the series in the legend, the readout and the table. |
|---|---|
points readonly (LinePoint | null)[] | One point per x position, aligned with |
value number | The y value, to draw. |
|---|---|
valueLabel string | The y value, formatted, for the readout and the table: "$84.20". |
noScript string | Shown with scripts off, above the table: "Scripts are off, so the chart's data is in the table below." |
|---|---|
roleDescription string | What a screen reader calls the plot, its |
showChart string | |
showTable string |
empty string | Drawn in the chart when |
|---|---|
error string | Drawn in the chart when |
loading string | The polite status while loading: "Loading". |
Accessibility
The chart is a figure named by aria-label or aria-labelledby, which the types require. The plot is a single tab stop, so thirty-one days are never thirty-one stops. It is a group named like the figure, with the role description "chart" (labels.roleDescription), and becomes focusable once scripts run. The table is the full alternative.
- Keys: the arrows move a readout and a crosshair through the days with data. Keys with Alt, Control or Meta are left to the browser.
- Readout: it sits above the day's dots, or below them near the top, so it never covers its point.
- Announcements: once the keys rest for 150ms, a screen reader hears the day and each series with its value, so a held key is heard only where it stops. Each part is announced on its own, with no punctuation added between your words.
- Pointer: snaps to the nearest day anywhere in its column, not only on the line.
- Forced colors: the first line uses the reader's text color, the second line and gridlines GrayText, and the crosshair and dots Highlight.
| Tab | Moves to the plot, one stop for the whole chart, then to the table toggle. |
|---|---|
| LeftRight | The previous or next day with data, in the drawn direction. |
| HomeEnd | The first or last day with data. |
| PageUpPageDown | Seven days back or forward. |
| Escape | Hides the readout until the next move. A second Escape goes on to the dialog or sheet around the chart. |
| EnterSpace | On the toggle, swaps the plot and the table. |
Design rules
What keeps line charts in the system.
Draw one story line, and at most one line of context.
Label the gridlines the reader needs to read a value, and no more.
Give charts read side by side the same yDomain.
Never draw more than two series.
Never smooth the line. A curve invents values between the days.
Never tell two series apart by red and green.
Never let the library format a figure. Every label is your string.