Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
← All tutorials · ~15 minutes · Layer focus: custom elements & web components
Build an admin dashboard with four KPI tiles, a live chart, and a sortable table. The layout is semantic HTML, the cards are custom elements, and the chart and table are web components — each layer pulling its weight.
A dashboard is just a page with a heading, four numbers, a chart, and a table. In semantic HTML that's exactly what we write — no framework vocabulary required.
<main> <header> <h1>Dashboard</h1> <button type="button">Invite</button> </header> <section aria-label="Key metrics"> <article><h2>Signups</h2><p>1,284</p></article> <article><h2>Active users</h2><p>8,927</p></article> <article><h2>Revenue</h2><p>$42.3k</p></article> <article><h2>Churn</h2><p>2.1%</p></article> </section> <section> <h2>Page views this week</h2> <!-- chart will live here --> </section> <section> <h2>Recent projects</h2> <table> <thead><tr><th>Project</th><th>Owner</th><th>Status</th></tr></thead> <tbody> <tr><td>Website Redesign</td><td>Alice</td><td>Active</td></tr> <tr><td>Mobile App</td><td>Bob</td><td>In review</td></tr> </tbody> </table> </section></main>
data-*Four KPIs in a responsive row? That's data-layout="grid" with a sensible data-layout-min. The header is a cluster. Nothing else changes.
<main data-layout="stack" data-layout-gap="l"> <header data-layout="cluster" data-layout-justify="between" data-layout-align="center"> <h1>Dashboard</h1> <nav data-layout="cluster" data-layout-gap="s"> <button type="button" class="secondary">Export</button> <button type="button">Invite</button> </nav> </header> <section data-layout="grid" data-layout-min="180px" data-layout-gap="m" aria-label="Key metrics"> <article> <p>Signups</p> <p>1,284</p> <p>▲ 12.4%</p> </article> <!-- three more articles… --> </section></main>
One attribute per section, all on the elements we already had.
<layout-card>A KPI tile is a surface with a border, padding, and a consistent inner rhythm. That's what <layout-card> is for. You get the visual treatment, keep the semantic <article> meaning, and gain no JavaScript.
<section data-layout="grid" data-layout-min="180px" data-layout-gap="m" aria-label="Key metrics"> <layout-card data-variant="outlined" data-padding="m"> <p>Signups</p> <p>1,284</p> <p>▲ 12.4%</p> </layout-card> <layout-card data-variant="outlined" data-padding="m"> <p>Active users</p> <p>8,927</p> <p>▲ 4.1%</p> </layout-card> <layout-card data-variant="outlined" data-padding="m"> <p>Revenue</p> <p>$42.3k</p> <p>▲ 8.7%</p> </layout-card> <layout-card data-variant="outlined" data-padding="m"> <p>Churn</p> <p>2.1%</p> <p>▼ 0.3%</p> </layout-card></section>
<layout-card> variants: outlined, elevated, ghost. Padding scale: s, m, l. The whole component is CSS — no runtime cost.
Two places on this page genuinely benefit from JavaScript: the chart (rendering to canvas) and the data table (sorting, filtering). For everything else we stayed declarative.
<chart-wc> is a thin Chart.js wrapper driven entirely by data attributes. Pass a JSON data-values payload and you get an accessible, themed, responsive chart that respects the current theme tokens.
<section data-layout="stack" data-layout-gap="s"> <h2>Page views this week</h2> <layout-card data-variant="outlined" data-padding="m"> <chart-wc data-type="area" data-label-y="Views" data-legend data-tooltip data-values='[ {"name": "Organic", "values": {"Mon": 3200, "Tue": 4100, "Wed": 3800, "Thu": 5200, "Fri": 4700, "Sat": 2400, "Sun": 2100}}, {"name": "Direct", "values": {"Mon": 1200, "Tue": 1400, "Wed": 1100, "Thu": 1800, "Fri": 2200, "Sat": 900, "Sun": 800}} ]'> </chart-wc> </layout-card></section>
To use <chart-wc>, add one CSS and one JS file to your page:
<link rel="stylesheet" href="https://unpkg.com/vanilla-breeze/dist/cdn/vanilla-breeze-charts.css"/><script type="module" src="https://unpkg.com/vanilla-breeze/dist/cdn/vanilla-breeze-charts.js"></script>
Supported types: line, area, bar, column, pie, scatter, bubble.
A plain <table> already has semantics and a readable default style. Wrap it in <data-table> and every column header becomes clickable, sortable, and announces its state to screen readers — without you writing any JavaScript.
<data-table sortable> <table> <thead> <tr><th>Project</th><th>Owner</th><th>Status</th><th>Tasks</th></tr> </thead> <tbody> <tr><td>Website Redesign</td><td>Alice</td><td>Active</td><td>12</td></tr> <tr><td>Mobile App</td><td>Bob</td><td>In review</td><td>8</td></tr> <tr><td>Design System</td><td>Carol</td><td>Active</td><td>23</td></tr> <tr><td>Marketing Site</td><td>Dan</td><td>Shipped</td><td>5</td></tr> </tbody> </table></data-table>
If JavaScript fails to load, the plain <table> is still perfectly readable. That's the whole point.
data-layout="grid" data-layout-min="180px" gives you a KPI row that reflows on any screen.<layout-card data-variant="outlined"> is the declarative way to spend a surface.<chart-wc> accepts a JSON data-values attribute — no imperative setup.<data-table> wraps a native <table> and degrades to it gracefully without JS.