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

Follows the page theme and corners
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

  • This month
  • Last month
Follows the page theme and corners
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

Follows the page theme and corners
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

  • This month
  • Last month
Follows the page theme and corners
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 statusContent with 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

No spending yet

November's spending shows here once a transaction arrives.

The chart could not be loaded

Check your connection, then try again.

Follows the page theme and corners
'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/core

It 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. valueLabel is what the reader sees, and value only 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, where no label or text is shown.

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 labels, so the four labels stay as they were.

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 xLabels. null is a gap, such as a future day.

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 aria-roledescription: "chart".

showChart
string

showTable
string

empty
string

Drawn in the chart when status is 'empty' and no statusContent is given: "Nothing to show for this period".

error
string

Drawn in the chart when status is 'error' and no statusContent is given: "The chart could not be loaded".

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.
TabMoves to the plot, one stop for the whole chart, then to the table toggle.
LeftRightThe previous or next day with data, in the drawn direction.
HomeEndThe first or last day with data.
PageUpPageDownSeven days back or forward.
EscapeHides the readout until the next move. A second Escape goes on to the dialog or sheet around the chart.
EnterSpaceOn 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.

esc

↑ ↓ to move↵ to open