Skip to content

ExpressiveCSS

Browse documentation

Line chart

Trends over time drawn from a data table, with a tooltip, a legend and a sparkline size.

View as Markdown

A <figure class="line-chart"> draws a <table> of values as smooth lines. The first column labels the x axis. Each further column is a series named by its header cell.

JavaScript draws the chart from the table. It hides the table visually, so a screen reader still reads the data as a table. Without JavaScript, the table shows. Points, a cursor and a tooltip follow the pointer, and the arrow keys move them when the chart has focus.

Total revenue
$54,500
+6.2% from last year
Monthly revenue, 2026
MonthRevenue
Jan$4,400
Feb$3,400
Mar$3,700
Apr$4,900
May$4,600
Jun$5,000
Jul$5,500
Aug$4,000
Sep$4,500
Oct$5,000
Nov$4,000
Dec$5,500
<figure class="line-chart fade">
  <figcaption>Monthly revenue, 2026</figcaption>
  <table>
    <thead>
      <tr><th>Month</th><th>Revenue</th></tr>
    </thead>
    <tbody>
      <tr><th>Jan</th><td>$4,400</td></tr>
      <tr><th>Feb</th><td>$3,400</td></tr>
      <tr><th>Mar</th><td>$3,700</td></tr>
      <!-- One row per month -->
    </tbody>
  </table>
</figure>

fade fades the ends of each line. The tooltip shows each cell's text as written, so format values in the table.

Area

area fills under each line with a gradient in the series color.

Monthly revenue, first half
MonthRevenue
Jan$4,400
Feb$3,400
Mar$3,700
Apr$4,900
May$4,600
Jun$5,000
<figure class="line-chart area">
  <figcaption>Monthly revenue, first half</figcaption>
  <table><!-- … --></table>
</figure>

Several series

Each value column after the first is another series. A legend appears when there is more than one. Series take primary, tertiary, secondary and on-surface-variant in turn. Classes on a series header cell apply to its line, so <th class="dashed"> draws a target or forecast as a dotted line.

Revenue against last year and target
MonthThis yearLast yearTarget
Jan$4,400$4,000$5,200
Feb$3,400$4,400$5,400
Mar$3,700$3,100$5,100
Apr$4,900$4,500$5,000
May$4,600$4,900$5,700
Jun$5,000$4,000$5,500
Jul$5,500$4,000$6,200
Aug$4,000$4,400$6,400
Sep$4,500$3,100$6,100
Oct$5,000$4,500$5,000
Nov$4,000$4,900$6,700
Dec$5,500$5,500$6,500
<figure class="line-chart">
  <figcaption>Revenue against last year and target</figcaption>
  <table>
    <thead>
      <tr><th>Month</th><th>This year</th><th>Last year</th><th class="dashed">Target</th></tr>
    </thead>
    <tbody>
      <tr><th>Jan</th><td>$4,400</td><td>$4,000</td><td>$5,200</td></tr>
      <!-- One row per month -->
    </tbody>
  </table>
</figure>

Legend toggle

Add toggle to let readers hide a series. Each legend item becomes a button, pressed while its series shows. Pressing one hides that series from the plot and the tooltip, and the scale refits to the series left. The others keep their colors. The last series shown cannot be hidden. A hidden series is struck through, with a hollow swatch, and stays hidden through update().

Revenue against last year and target
MonthThis yearLast yearTarget
Jan$4,400$4,000$5,200
Feb$3,400$4,400$5,400
Mar$3,700$3,100$5,100
Apr$4,900$4,500$5,000
May$4,600$4,900$5,700
Jun$5,000$4,000$5,500
Jul$5,500$4,000$6,200
Aug$4,000$4,400$6,400
Sep$4,500$3,100$6,100
Oct$5,000$4,500$5,000
Nov$4,000$4,900$6,700
Dec$5,500$5,500$6,500
<figure class="line-chart axis toggle">
  <figcaption>Revenue against last year and target</figcaption>
  <table><!-- As above --></table>
</figure>

Points and straight lines

Add points to mark every value with a dot, so a reader can see where the measurements are. Add straight to draw straight lines between them instead of a smooth curve. Both suit a few values read one by one, such as a week of visits.

Visitors this week
DayVisitors
Mon8,200
Tue9,400
Wed10,100
Thu9,800
Fri11,600
Sat12,400
Sun11,900
<figure class="line-chart axis points straight">
  <figcaption>Visitors this week</figcaption>
  <table>
    <thead>
      <tr><th>Day</th><th>Visitors</th></tr>
    </thead>
    <tbody>
      <tr><th>Mon</th><td>8,200</td></tr>
      <!-- … -->
    </tbody>
  </table>
</figure>

Stacked

Add stacked to draw each series on top of the ones before it and fill the band between them. The top line is the total. The y axis starts at zero, and the tooltip still shows each cell. Stack values that add up, such as sales by channel, and keep them positive. An empty cell leaves the total unknown, so it breaks that series and the ones above it at that row.

Sales by channel, first half
MonthOnlineRetailWholesale
Jan$2,100$1,800$900
Feb$2,300$1,700$1,000
Mar$2,200$1,900$950
Apr$2,600$1,800$1,100
May$2,800$2,000$1,050
Jun$3,100$2,100$1,200
<figure class="line-chart stacked">
  <figcaption>Sales by channel, first half</figcaption>
  <table>
    <thead>
      <tr><th>Month</th><th>Online</th><th>Retail</th><th>Wholesale</th></tr>
    </thead>
    <tbody>
      <tr><th>Jan</th><td>$2,100</td><td>$1,800</td><td>$900</td></tr>
      <!-- One row per month -->
    </tbody>
  </table>
</figure>

Sparkline

Use sparkline for a trend beside a figure. It is a 48px chart with no grid, labels, legend or tooltip. Hide its caption with visually-hidden when the tile already names it.

Balance
$5,000
+25% over 14 days
Balance over 14 days
DayBalance
Day 1$4,000
Day 2$4,500
Day 3$4,300
Day 4$4,400
Day 5$4,900
Day 6$4,600
Day 7$5,000
Day 8$4,900
Day 9$4,500
Day 10$4,300
Day 11$4,400
Day 12$4,900
Day 13$4,600
Day 14$5,000
Orders
452
+46% over 14 days
Orders over 14 days
DayOrders
Day 1310
Day 2290
Day 3335
Day 4320
Day 5360
Day 6340
Day 7380
Day 8372
Day 9395
Day 10410
Day 11398
Day 12430
Day 13425
Day 14452
<dl class="stats">
  <div>
    <dt>Balance</dt>
    <dd>$5,000</dd>
    <dd class="stat-change up">+25% over 14 days</dd>
    <dd>
      <figure class="line-chart sparkline area my-0">
        <figcaption class="visually-hidden">Balance over 14 days</figcaption>
        <table><!-- … --></table>
      </figure>
    </dd>
  </div>
</dl>

Chart cards

Put a chart in a card with the figures it explains. To add a period switch:

  1. Put the switch beside the stat.
  2. On a change, update the stat and swap the table rows.
  3. With data-watch on the figure, the chart redraws itself. Without it, call update().

The card adds no chart styles, so size a chart with its tokens. A card pads only its headings and paragraphs. Wrap the stat and chart in <div class="p-4"> to keep them off the border. A connected button group fills its container, so give it a <div> of its own to keep it beside the stat.

Revenue
$54,500
+6.2% from last year
Revenue this year and last year
MonthThis yearLast year
Jan$4,400$4,000
Feb$3,400$4,400
Mar$3,700$3,100
Apr$4,900$4,500
May$4,600$4,900
Jun$5,000$4,000
Jul$5,500$4,000
Aug$4,000$4,400
Sep$4,500$3,100
Oct$5,000$4,500
Nov$4,000$4,900
Dec$5,500$5,500
<article class="outlined" id="revenue-card">
  <div class="p-4">
    <div class="flex flex-wrap justify-between align-start gap-3">
      <dl class="stats my-0">
        <div>
          <dt>Revenue</dt>
          <dd>$54,500</dd>
          <dd class="stat-change up">+6.2% from last year</dd>
        </div>
      </dl>
      <div>
        <div class="button-group connected xsmall" data-selection="single"
             data-selection-required role="group" aria-label="Period">
          <button type="button" class="button tonal" aria-pressed="true"
                  data-period="12" data-total="$54,500"
                  data-change="+6.2% from last year">Year</button>
          <button type="button" class="button tonal" aria-pressed="false"
                  data-period="6" data-total="$28,500"
                  data-change="+8.0% from last year">6 months</button>
        </div>
      </div>
    </div>
    <figure class="line-chart area" data-watch>
      <figcaption class="visually-hidden">Revenue this year and last year</figcaption>
      <table><!-- … --></table>
    </figure>
  </div>
</article>

<script>
  const card = document.getElementById('revenue-card');
  // The chart has data-watch, so it redraws itself when the rows change.
  const body = card.querySelector('.line-chart tbody');
  const rows = [...body.rows];
  card.querySelectorAll('[data-period]').forEach((button) => {
    button.addEventListener('click', () => {
      body.replaceChildren(...rows.slice(-Number(button.dataset.period)));
      card.querySelector('.stats dd').textContent = button.dataset.total;
      card.querySelector('.stat-change').textContent = button.dataset.change;
    });
  });
</script>

For a compact card, set the height and clear the grid. A sparkline with flex-1 beside the figures fills the rest of a short tile.

Visitors this week
73,400
+14.5% from last week
Visitors per day this week
DayVisitors
Mon8,200
Tue9,400
Wed10,100
Thu9,800
Fri11,600
Sat12,400
Sun11,900
Balance
$5,000
+25% over 14 days
Balance over 14 days
DayBalance
Day 1$4,000
Day 2$4,500
Day 3$4,300
Day 4$4,400
Day 5$4,900
Day 6$4,600
Day 7$5,000
Day 8$4,900
Day 9$4,500
Day 10$4,300
Day 11$4,400
Day 12$4,900
Day 13$4,600
Day 14$5,000
<article class="outlined">
  <div class="p-4">
    <dl class="stats my-0">…</dl>
    <figure class="line-chart area" style="
        --md-comp-line-chart-height: 120px;
        --md-comp-line-chart-line-width: 2px;
        --md-comp-line-chart-grid-color: transparent">
      <figcaption class="visually-hidden">Visitors per day this week</figcaption>
      <table><!-- … --></table>
    </figure>
  </div>
</article>

<article class="outlined">
  <div class="p-4">
    <div class="flex align-center justify-between gap-3">
      <dl class="stats my-0">…</dl>
      <figure class="line-chart sparkline area flex-1 my-0"
              style="--md-comp-line-chart-height: 72px">
        <figcaption class="visually-hidden">Balance over 14 days</figcaption>
        <table><!-- … --></table>
      </figure>
    </div>
  </div>
</article>

Values and scale

A cell's number is its text without currency signs, percent signs or grouping commas, so $4,400 reads as 4400. Text that is not a single number, such as 4.4k or (1,200), is a gap in the line, as is an empty cell. Put the number in data-value for those, and for 4.400 €, which would read as 4.4.

The y axis spans the data with 10% to spare above and below. Set data-min or data-max on the figure to fix either end, such as data-min="0" when the change should be read against zero.

Add axis to label the y axis at round values, with a grid line at each. The chart widens an end it chose to the next round value, so the labels start and end on the plot's edges. An end set with data-min or data-max stays where it is. The labels repeat any currency or percent sign the cells are written with, and shorten large numbers, such as $1.5K. They are hidden from assistive technology, and the table still carries the values.

Monthly revenue, first half
MonthRevenue
Jan$4,400
Feb$3,400
Mar$3,700
Apr$4,900
May$4,600
Jun$5,000
<figure class="line-chart axis">
  <figcaption>Monthly revenue, first half</figcaption>
  <table><!-- … --></table>
</figure>
<figure class="line-chart" data-min="0">
  <figcaption>Signups</figcaption>
  <table>
    <thead><tr><th>Week</th><th>Signups</th></tr></thead>
    <tbody>
      <tr><th>Week 1</th><td data-value="4400">4.4k</td></tr>
      <tr><th>Week 2</th><td></td></tr>
      <tr><th>Week 3</th><td data-value="5100">5.1k</td></tr>
    </tbody>
  </table>
</figure>

Accessibility

The table is the chart's text alternative. Give it a header row and a header cell per row, and name the chart with a <figcaption>. The drawn chart, its labels and its legend are hidden from assistive technology. The exception is a toggle legend. Its buttons follow the plot in the tab order and say whether each series shows.

The plot is one Tab stop named by the caption. Left and right arrows move between rows. Home and End jump to the ends, and Escape hides the tooltip. In a right-to-left page, the x axis runs from right to left, and the arrows still move the cursor the way they point. The tooltip is a polite live region, so a screen reader hears each row's values as it moves. A sparkline is not focusable.

JavaScript

AutoInit() starts every .line-chart. To start one yourself, call LineChart.init. After you change its table, call update() to redraw it in place. The lines, areas and points move from their old values to the new ones. A series or value new to the chart rises from the plot bottom, or in a stack from the series below. With reduced motion, the new shape appears at once.

Add data-watch to the figure, or set the watch option, and the chart calls update() itself whenever its table changes. That covers rows or cells added or removed, cell text, and a data-value, class or heatmap data-label inside the table. A replaced table is watched too. Every chart type takes data-watch.

The edits a script makes in one run redraw the chart once. Edits on either side of an await redraw it twice. The chart reads the attribute when it starts, and any value turns it on. Changes to the figure itself, such as data-min, its classes or its caption, still need update().

const chart = Expressive.LineChart.init(document.querySelector('.line-chart'), {
  min: 0
});
chart.show(2);   // highlight the third row; -1 hides the tooltip
chart.update();  // redraw from the table; focus and the highlighted row stay
chart.destroy(); // remove the drawing and show the table again
OptionDefaultDescription
minnullLowest y value. data-min overrides it.
maxnullHighest y value. data-max overrides it.
watchfalseRedraw when the table changes. data-watch turns it on too.

Tokens

Override these on the .line-chart.

Token Default
--md-comp-line-chart-height240px, 48px for a sparkline
--md-comp-line-chart-line-width3px, 2px for a sparkline
--md-comp-line-chart-grid-color--md-sys-color-outline-variant
--md-comp-line-chart-axis-width48px, the room beside the plot for axis labels
--md-comp-line-chart-color-1--md-sys-color-primary
--md-comp-line-chart-color-2--md-sys-color-tertiary
--md-comp-line-chart-color-3--md-sys-color-secondary
--md-comp-line-chart-color-4--md-sys-color-on-surface-variant

For a selective Sass build, include "line-chart" in $components.

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow