motion-chart
Bar and line charts written in HTML. Put the numbers in an attribute or wrap a table. Bars grow and lines draw in when the chart scrolls into view. When the data changes, every bar and point springs to its new value.
Preview
Hover a bar, or press New data several times quickly. The bars turn around from wherever they are.
<motion-chart
values="12, 19, 8, 24, 16, 21, 14"
labels="Mon, Tue, Wed, Thu, Fri, Sat, Sun"
label="Orders per day"
></motion-chart>
<button id="new-data">New data</button>Set values from JavaScript to change the data. The chart springs to the new numbers:
const chart = document.querySelector('motion-chart')
const datasets = [
'12, 19, 8, 24, 16, 21, 14',
'20, 9, 17, 10, 27, 25, 14',
'8, 24, 15, 19, 11, 28, 22',
]
let step = 0
document.querySelector('#new-data').addEventListener('click', () => {
step = (step + 1) % datasets.length
chart.values = datasets[step]
})Quick start
Save this as index.html and open it in a browser. No install, no build step.
<!doctype html>
<html>
<head>
<script type="module" src="https://cdn.jsdelivr.net/npm/motion-components@1/dist/motion-chart.js/+esm"></script>
</head>
<body>
<motion-chart
values="12, 19, 8, 24, 16"
labels="Mon, Tue, Wed, Thu, Fri"
label="Orders per day"
></motion-chart>
</body>
</html>In a project with a bundler, install the package and import the component once:
npm install motion-componentsimport 'motion-components/motion-chart'Give the chart its data
There are three ways. Pick the one that fits where your data comes from.
1. Numbers in attributes
Separate the numbers with commas. For several series, separate the series with ; and name them in series.
<motion-chart
values="42, 55, 61, 48; 30, 38, 52, 60"
labels="Q1, Q2, Q3, Q4"
series="2025, 2026"
format="currency:EUR"
></motion-chart>2. A table
Wrap a normal <table>. The first column holds the labels, and every other column is one series. The header row names the
series. Screen readers still read the table, and the chart updates when a cell changes.
| Month | Visitors | Signups |
|---|---|---|
| Jan | 1200 | 310 |
| Feb | 1850 | 420 |
| Mar | 1600 | 530 |
| Apr | 2400 | 610 |
| May | 2900 | 780 |
| Jun | 3300 | 900 |
<motion-chart type="line" label="Visitors and signups">
<table>
<tr><th>Month</th><th>Visitors</th><th>Signups</th></tr>
<tr><td>Jan</td><td>1200</td><td>310</td></tr>
<tr><td>Feb</td><td>1850</td><td>420</td></tr>
<tr><td>Mar</td><td>1600</td><td>530</td></tr>
<tr><td>Apr</td><td>2400</td><td>610</td></tr>
<tr><td>May</td><td>2900</td><td>780</td></tr>
<tr><td>Jun</td><td>3300</td><td>900</td></tr>
</table>
</motion-chart>How a cell becomes a number:
- The first number in the cell is used, so units and notes around it are ignored:
€12kreads as 12,3 – 5%as 3. - Commas group thousands and a dot marks decimals:
1,234.5reads as 1234.5. - A cell without a number, such as
—, counts as 0. - For other formats, such as
1.234,5, put the number indata-value:<td data-value="1234.5">1.234,5</td>. It overrides the cell text.
3. From JavaScript
Set the data property. Every time you set it, the chart springs to the new values.
const chart = document.querySelector('motion-chart')
chart.data = {
labels: ['Jan', 'Feb', 'Mar'],
series: [
{ name: 'Visitors', values: [1200, 1850, 1600] },
{ name: 'Signups', values: [310, 420, 530] },
],
}For parts of a whole, use motion-pie, which draws pie and donut charts.
Live data
Change values, data or a table cell, and the chart moves to the new numbers. This line gets a new value every 1.5 seconds.
<motion-chart id="live" type="line" values="40, 42, 45, 43, 48, 52, 50, 55"></motion-chart>
<script type="module">
const chart = document.querySelector('#live')
setInterval(() => {
const values = chart.values.split(',').map(Number)
const next = values.at(-1) + Math.round(Math.random() * 10 - 4)
chart.values = [...values.slice(1), next].join(', ')
}, 1500)
</script>
When the range changes, the scale and its grid lines spring to the new range. Old grid lines fade out and new ones fade in. Set
animate-scale="false" to switch the scale at once.
<motion-chart type="line" values="40, 42, 45, 43" animate-scale="false"></motion-chart>Number format
format formats the axis, the tooltip and the screen reader text in the reader's language.
| | 12900 shows as |
|---|---|
| (empty) | 12,900 |
| | 12.9K |
| | €12,900 |
| | 12,900% |
| | 12,900 km |
percent adds a percent sign. It does not multiply by 100, so write 12 for 12%.
More patterns, such as a period switcher, a chart from a form and data from a JSON file, are on the Chart Recipes page.
Frameworks
Write the data into attributes, or wrap a table.
<script type="module" src="https://cdn.jsdelivr.net/npm/motion-components@1/dist/motion-chart.js/+esm"></script>
<motion-chart values="12, 19, 8, 24" labels="Mon, Tue, Wed, Thu"></motion-chart>React 19 sets data as a property, so pass the object from state. The chart springs to every new value.
import 'motion-components/motion-chart'
export function SalesChart({ months, sales }) {
const data = { labels: months, series: [{ name: 'Sales', values: sales }] }
return <motion-chart type="line" format="compact" data={data}></motion-chart>
}Tell Vue that motion-* tags are custom elements (see Getting started), then bind :data.
<script setup>
import 'motion-components/motion-chart'
const props = defineProps(['months', 'sales'])
</script>
<template>
<motion-chart
type="line"
format="compact"
:data="{ labels: props.months, series: [{ name: 'Sales', values: props.sales }] }"
></motion-chart>
</template>Colors and size
The eight series colors are chosen to stay apart for color-blind readers, in light and dark mode. Override them with --mc-chart-1 to
--mc-chart-8. Text and gridlines follow the text color.
<motion-chart
values="8, 14, 11, 19"
labels="North, East, South, West"
style="--mc-chart-1: #16a34a; --mc-chart-height: 12rem"
></motion-chart>Properties
| Attribute | Property | Type | Default | Description |
|---|---|---|---|---|
| | | string | 'bar' | |
| | | string | '' | Comma-separated numbers. Separate several series with |
| | | string | '' | Comma-separated labels for |
| | | string | '' | Comma-separated series names for |
| | | object | null | Property only: |
| | | string | '' | |
| | | number | auto | Lowest value on the axis. Defaults to the lower of 0 and the smallest value |
| | | number | auto | Highest value on the axis. Defaults to the largest value, rounded up |
| | | boolean | true | Springs the scale and grid lines to the new range when the data changes. Set |
| | | string | '' | Accessible name of the chart |
| | | string | 'view' | When the entrance runs: |
| | | number | 0.8 | Spring duration in seconds |
| | | number | 0.25 | Spring bounciness (0 = no overshoot) |
| | | number | 0.04 | Seconds between one bar group and the next on entrance |
| | | boolean | true | Set |
| | | boolean | true | Shown for two or more series. Set |
CSS custom properties
| Property | Type | Default | Description |
|---|---|---|---|
| | length | 16rem | Height of the chart, including the legend |
| | color | blue, orange, aqua, yellow, magenta, green, violet, red | Series colors, in order |
| | color | Canvas | Background of the tooltip |
JavaScript API
replay() runs the entrance again. Like every animated component, it also has play(), pause(),
finish(), cancel(), playState and the motion-start and motion-finish events. See
the JavaScript API.
Accessibility
Screen readers get the data as a table: your own table when you wrap one, or a generated one for values and data. Give
the chart a label. Press Tab to focus the chart, then ← and → to move between points,
Home and End for the first and last, and Esc to close the tooltip. Each point is announced with its values.
With prefers-reduced-motion, the chart shows its final state and changes values at once.