Chart Recipes
Everyday data patterns built with motion-chart,
motion-pie and motion-sparkline. Each
snippet is complete: copy it into a page that loads the library.
<script type="module" src="https://cdn.jsdelivr.net/npm/motion-components@1/+esm"></script>Stat tile
A number that counts up above its trend. motion-counter animates the value, motion-sparkline draws the trend.
<div class="stat">
<span class="stat-label">Revenue this month</span>
<motion-counter to="48.2" decimals="1" prefix="€" suffix="K"></motion-counter>
<motion-sparkline values="31, 35, 33, 38, 41, 39, 45, 48" area></motion-sparkline>
</div>
<style>
.stat {
display: grid;
gap: 0.25rem;
width: 16rem;
font-size: 2rem;
font-weight: 700;
}
.stat-label {
font-size: 0.85rem;
font-weight: 500;
opacity: 0.7;
}
.stat motion-sparkline {
--mc-sparkline-width: 100%;
--mc-sparkline-height: 2.5rem;
color: #2a78d6;
}
</style>Week, month and year switcher
Buttons swap the data and the color, and the bars spring to the new values while the axis follows. Click quickly between periods: the bars turn around from wherever they are.
<div class="period-chart">
<div class="periods" role="group" aria-label="Period">
<button aria-pressed="true" data-period="week">Week</button>
<button aria-pressed="false" data-period="month">Month</button>
<button aria-pressed="false" data-period="year">Year</button>
</div>
<motion-chart id="visits" label="Visits" labels="Mon, Tue, Wed, Thu, Fri, Sat, Sun" values="320, 410, 380, 460, 520, 290, 250"></motion-chart>
</div>
<script type="module">
const chart = document.querySelector('#visits')
const periods = {
week: { color: '#2a78d6', labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], values: [320, 410, 380, 460, 520, 290, 250] },
month: { color: '#1baf7a', labels: ['W1', 'W2', 'W3', 'W4'], values: [2400, 2900, 2650, 3100] },
year: { color: '#8b5cf6', labels: ['Q1', 'Q2', 'Q3', 'Q4'], values: [9800, 11200, 10400, 12900] },
}
document.querySelectorAll('[data-period]').forEach((button) => {
button.addEventListener('click', () => {
document.querySelectorAll('[data-period]').forEach((b) => b.setAttribute('aria-pressed', b === button))
const { color, labels, values } = periods[button.dataset.period]
chart.style.setProperty('--mc-chart-1', color)
chart.data = { labels, series: [{ name: 'Visits', values }] }
})
})
</script>
<style>
.period-chart {
display: grid;
gap: 1rem;
width: 100%;
}
.periods {
display: flex;
gap: 0.5rem;
}
</style>Chart from a form
Type a number and the matching bar springs to it. The inputs are the data, so the chart never needs a separate update call.
<div class="form-chart">
<div class="inputs">
<label>North <input type="number" value="8" data-region></label>
<label>East <input type="number" value="14" data-region></label>
<label>South <input type="number" value="11" data-region></label>
<label>West <input type="number" value="19" data-region></label>
</div>
<motion-chart id="regions" label="Sales by region" labels="North, East, South, West" values="8, 14, 11, 19"></motion-chart>
</div>
<script type="module">
const chart = document.querySelector('#regions')
const inputs = [...document.querySelectorAll('[data-region]')]
for (const input of inputs) {
input.addEventListener('input', () => {
chart.values = inputs.map((i) => Number(i.value) || 0).join(', ')
})
}
</script>
<style>
.form-chart {
display: grid;
gap: 1rem;
width: 100%;
}
.inputs {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.inputs label {
display: grid;
gap: 0.25rem;
font-size: 0.85rem;
}
</style>Load data from a JSON file
Fetch the data and set data. The chart waits with empty axes, then animates in once the data arrives. This demo loads /data/sales.json from this site.
<motion-chart id="sales" type="line" label="Sales" format="currency:EUR"></motion-chart>
<script type="module">
const chart = document.querySelector('#sales')
const response = await fetch('/data/sales.json')
chart.data = await response.json()
</script>
<style>
#sales {
width: 100%;
}
</style>sales.json has the same shape as data: { "labels": [...], "series": [{ "name": "Online", "values": [...] }] }.
Budget donut by month
A select changes the month, and the slices spring to their new sizes. The center shows the total, or the slice you hover.
<div class="budget">
<select id="month" aria-label="Month">
<option value="jan">January</option>
<option value="feb">February</option>
<option value="mar">March</option>
</select>
<motion-pie id="budget" donut label="Monthly budget" format="currency:EUR" labels="Rent, Food, Transport, Fun" values="1450, 620, 380, 240"></motion-pie>
</div>
<script type="module">
const pie = document.querySelector('#budget')
const months = {
jan: [1450, 620, 380, 240],
feb: [1450, 540, 310, 420],
mar: [1450, 700, 260, 180],
}
document.querySelector('#month').addEventListener('change', (event) => {
pie.values = months[event.target.value].join(', ')
})
</script>
<style>
.budget {
display: grid;
justify-items: start;
gap: 1rem;
width: 100%;
}
#budget {
width: 100%;
}
</style>Color palettes
The colors are CSS custom properties, --mc-chart-1 to --mc-chart-8. Set them on a wrapper, and every chart inside takes them. These buttons switch palettes for a bar chart and a donut at once.
<div id="themed" class="themed-charts">
<div class="palettes" role="group" aria-label="Color palette">
<button aria-pressed="true" data-palette="default">Default</button>
<button aria-pressed="false" data-palette="ocean">Ocean</button>
<button aria-pressed="false" data-palette="sunset">Sunset</button>
<button aria-pressed="false" data-palette="mono">Mono</button>
</div>
<motion-chart label="Sales by channel" labels="Q1, Q2, Q3, Q4" series="Web, Store, Partners" values="42, 55, 61, 70; 30, 38, 35, 41; 12, 18, 25, 31"></motion-chart>
<motion-pie donut label="Sales by channel" labels="Web, Store, Partners" values="228, 144, 86"></motion-pie>
</div>
<script type="module">
const charts = document.querySelector('#themed')
const palettes = {
default: [],
ocean: ['#0369a1', '#0ea5e9', '#14b8a6'],
sunset: ['#c2410c', '#f59e0b', '#be185d'],
mono: ['#18181b', '#71717a', '#c4c4cc'],
}
document.querySelectorAll('[data-palette]').forEach((button) => {
button.addEventListener('click', () => {
document.querySelectorAll('[data-palette]').forEach((b) => b.setAttribute('aria-pressed', b === button))
const colors = palettes[button.dataset.palette]
for (let i = 1; i <= 3; i++) {
if (colors[i - 1]) charts.style.setProperty(`--mc-chart-${i}`, colors[i - 1])
else charts.style.removeProperty(`--mc-chart-${i}`)
}
})
})
</script>
<style>
.themed-charts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
width: 100%;
}
.palettes {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
grid-column: 1 / -1;
}
</style>Without JavaScript, set the palette in CSS: .my-charts { --mc-chart-1: #0369a1; --mc-chart-2: #0ea5e9; }. The default palette is chosen so the colors stay apart for readers with color blindness. When you pick your own, make the colors differ in lightness too, as the Mono palette does.
Live line chart
A new point every 1.5 seconds. The oldest point drops off, and the whole line springs into its new shape.
<motion-chart id="live-cpu" type="line" label="CPU load" format="percent" min="0" max="100" values="42, 48, 45, 51, 49, 55, 53, 58"></motion-chart>
<script type="module">
const chart = document.querySelector('#live-cpu')
setInterval(() => {
const values = chart.values.split(',').map(Number)
const next = Math.min(100, Math.max(0, values.at(-1) + Math.round(Math.random() * 14 - 7)))
chart.values = [...values.slice(1), next].join(', ')
}, 1500)
</script>
<style>
#live-cpu {
width: 100%;
}
</style>