Every time I needed a chart on a dashboard, the playbook was the same: pull in Chart.js or Recharts, fight with canvas sizing, write a wrapper component, and ship 40-80kb of JS just to draw a line. So with st-core.fscss — a library for FSCSS, CSS preprocessor — I went the other way: charts that are pure clip-path: polygon() shapes, animated with native CSS transitions, and driven entirely by CSS custom properties.
To prove it holds up outside a toy demo, I built Vela, a full portfolio/trading dashboard UI — KPI cards, a live performance chart with a benchmark overlay, allocation bars, sparklines, the works. You can poke at it live here:
🔗 Live preview: https://i.devtem.org/preview?id=5b5e4957-765b-4969-b1a0-3a4d0de495b4
No build step, no bundler — just index.html, dashboard.fscss, and main.js.
The core idea
A line chart is just a shape. If you have 8 data points on a 0-100 scale, you can describe the filled area under that line as a clip-path: polygon() with 8+2 coordinate pairs. st-core generates that polygon for you from CSS custom properties:
@st-chart-fill(.chart-fill)
@st-chart-line(.chart-line)
@st-chart-dots(.chart-dot-, 9px)
@st-chart-grid(.chart-grid, 6, 8)
.chart {
@st-chart-points(28, 42, 35, 55, 48, 72, 60, 78)
}
Those four @st-chart-* directives wire up a filled area, a stroked line, positioned dot markers, and a background grid — all from the same underlying point data. Swap the numbers in @st-chart-points, or update the matching --st-p1 through --st-p8 custom properties at runtime, and the shape redraws.
JS never touches a canvas
This is the part that actually matters for integration. main.js in Vela has zero charting logic — it only ever writes custom properties:
function setChartData(el, vals) {
vals.forEach((v, i) => {
el.style.setProperty(`--st-p${i + 1}`, `${100 - v}%`);
});
}
Because the chart shapes are driven by transition: clip-path 700ms cubic-bezier(.4,0,.2,1), animating between datasets is just... setting new values. There's a small easing loop in animateTo() for smoother interpolation, but even that is optional — CSS will tween clip-path changes on its own.
animateTo(mainChart, oldPoints, newPoints, 700);
Swap a REST call in and you have live data:
const res = await fetch('/api/portfolio/history?period=1w');
const { points } = await res.json();
setChartData(document.getElementById('chart-line-portfolio'), points);
Allocation bars and stat cards work the same way
st-core ships a few other primitives that follow the same pattern — a directive that hooks a custom property to a visual property:
@st-cat-bar-fill(.bar-fill)
@st-stat-card(.st-stat-card)
function setBar(id, pct) {
document.getElementById(id).style.setProperty('--st-cat-bar-fill-range', `${pct}%`);
}
One CSS variable in, one bar width out. No JS animation library needed because the width transition lives in the stylesheet.
Why this matters
A few things fall out of this approach almost for free:
- No JS dependency for rendering. The charts render even before main.js runs — they're just CSS shapes with default values baked into the stylesheet.
-
Themeable by design. Every color in Vela is a custom property (
--st-accent,--st-green,--st-red...). Retheme the whole dashboard by overriding tokens in:root. -
Animations are declarative. Hover tooltips, dot reveals, fade-ins — all
transitionand@keyframes, not requestAnimationFrame spaghetti (except where I wanted extra easing control). - Tiny footprint. No charting library payload. st-core compiles in the browser via the FSCSS runtime, or precompiles to plain CSS if you don't want runtime compilation at all.
Try it
The live preview link above is the full zip running as-is — sidebar nav, period switching (1D/1W/1M/3M/1Y), live-ticking BTC/ETH sparklines, the lot. Click around the period tabs and watch the chart redraw with the same clip-path mechanism described above.
Github repository: https://github.com/fscss-ttr/st-core.fscss
st-core.fscss is an open source MIT Licensed if you want to dig into the source, and Vela itself is listed as a template over on DevTemple if you'd rather skip straight to wiring up your own data.
Questions, pushback, "this is insane, why not just use a library"
Top comments (1)
This is brilliant! I'm curious about