st-core.fscss draws area, line, and spider/radar charts with pure CSS — clip-path: polygon() and custom properties, no SVG path builders, no canvas, no chart runtime on the critical path.

That design has a practical consequence for apps: updating a chart is rewriting CSS variables. Reactive frameworks already do that well. You do not need a JS chart instance, a virtual DOM subtree for every point, or a third-party wrapper’s internal state.

This article covers:

  • How data maps to CSS variables
  • Smooth transitions when values stream in
  • Patterns for Svelte, React, Vue, and plain JS

Requires FSCSS ≥ 1.2.3 and st-core@v2.

Source: github.com/fscss-ttr/st-core.fscss

How dynamic updates work

Linear charts

@st-chart-points(array) writes inverted Y positions on the host:

--st-p1, --st-p2, … --st-pN 

Fill, line, and dots only read those variables. Change the variables → the polygon and markers move.

Spider / radar charts

@st-spider-points(array, rmax) writes polar coordinates:

--st-sx1 / --st-sy1 … --st-sxN / --st-syN 

Same rule: points write, renderers read. Multi-series means one host per series so variables do not collide.

What you update at runtime

Chart Variables to set
Linear --st-p1 … --st-pN as percentages (top = 0%)
Spider --st-sxN / --st-syN via calc() + sin / cos

Geometry length (number of vertices) is fixed when FSCSS expands the mixins. Streaming updates should keep the same point count as the compiled polygon, or recompile with a new array length.

Smooth transitions

Because the visible shape is clip-path (and dots use top / left), the browser can interpolate on the compositor:

.chart-fill, .chart-line { transition: clip-path 0.3s ease; } .chart-dot-1, .chart-dot-2, .chart-dot-3, .chart-dot-4, .chart-dot-5, .chart-dot-6, .chart-dot-7, .chart-dot-8 { transition: top 0.3s ease, left 0.3s ease; } /* Spider markers */ .spider-dot-1, .spider-dot-2, .spider-dot-3, .spider-dot-4, .spider-dot-5, .spider-dot-6 { transition: top 0.3s ease, left 0.3s ease; } 

Note: Add transition: clip-path 0.3s ease, top 0.3s ease; (and left where needed) on the selectors that own the geometry. When new data points stream in, motion stays native and typically hits 60fps without a JS animation loop.

Optional:

.chart-fill, .chart-line { transition: clip-path 0.35s cubic-bezier(0.22, 1, 0.36, 1); } 

Helper: write variables from JS

Linear (0–100 scores → CSS %)

/** @param {number[]} values scores 0–100 */ export function linearVars(values) { return values .map((v, i) => `--st-p${i + 1}: ${100 - v}%`) .join(";"); } // host.style.cssText = linearVars([40, 55, 30, 70, 60]); // or: host.setAttribute("style", linearVars(values)); 

Spider / radar

/** @param {number[]} values scores 0–100 @param {number} rmax radius % of box */ export function polarVars(values, rmax = 42) { const n = values.length; return values .map((v, i) => { const ang = (i * 360) / n; const r = (v / 100) * rmax; return ( `--st-sx${i + 1}: calc(50% + (${r} * 1%) * sin(${ang} * 1deg));` + `--st-sy${i + 1}: calc(50% - (${r} * 1%) * cos(${ang} * 1deg));` ); }) .join(""); } 

Call these when props/store data change. No chart library, no requestAnimationFrame required for the interpolation itself.

Dynamic updates in modern frameworks

Because st-core operates on standard CSS variables, reactive frameworks can pass dataset values straight into the host’s style (or a style binding) without re-mounting chart components or syncing third-party chart state.

Svelte

Compile FSCSS once (CLI or runtime). In the component, bind variables when data changes:

<script> export let data = [40, 55, 30, 70, 60, 45, 80, 50]; function linearVars(values) { return values.map((v, i) => `--st-p${i + 1}: ${100 - v}%`).join(";"); } $: chartStyle = linearVars(data); </script> <div class="chart" style={chartStyle}> <div class="chart-fill"></div> <div class="chart-line"></div> {#each data as _, i} <div class="chart-dot-{i + 1}"></div> {/each} </div> <style> .chart-fill, .chart-line { transition: clip-path 0.3s ease; } :global([class^="chart-dot-"]) { transition: top 0.3s ease, left 0.3s ease; } </style> 

Stream updates:

data = await fetchLiveSeries(); // same length → smooth morph 

Spider host:

<div class="spider" style={polarVars(skills, 42)}> <div class="spider-grid"></div> <div class="spider-spokes"></div> <div class="spider-fill"></div> <div class="spider-line"></div> {#each skills as _, i} <div class="spider-dot-{i + 1}"></div> {/each} </div> 

React

import { useMemo } from "react"; function linearVars(values) { return values.map((v, i) => `--st-p${i + 1}: ${100 - v}%`).join(";"); } export function AreaChart({ data }) { const style = useMemo(() => linearVars(data), [data]); return ( <div className="chart" style={parseCssText(style)}> <div className="chart-fill" /> <div className="chart-line" /> {data.map((_, i) => ( <div key={i} className={`chart-dot-${i + 1}`} /> ))} </div> ); } /** cssText → React style object for custom properties */ function parseCssText(cssText) { return Object.fromEntries( cssText .split(";") .filter(Boolean) .map((pair) => { const [k, v] = pair.split(":"); return [k.trim(), (v ?? "").trim()]; }) ); } 

Or set the host with a ref (avoids parsing):

import { useEffect, useRef } from "react"; export function AreaChart({ data }) { const ref = useRef(null); useEffect(() => { if (!ref.current) return; ref.current.style.cssText = linearVars(data); }, [data]); return ( <div className="chart" ref={ref}> <div className="chart-fill" /> <div className="chart-line" /> {data.map((_, i) => ( <div key={i} className={`chart-dot-${i + 1}`} /> ))} </div> ); } 

Keep transition rules in your global or CSS-module sheet imported once with the st-core output.

Vue 3

<script setup> import { computed } from "vue"; const props = defineProps({ data: { type: Array, required: true }, }); function linearVars(values) { return values.map((v, i) => `--st-p${i + 1}: ${100 - v}%`).join(";"); } const chartStyle = computed(() => linearVars(props.data)); </script> <template> <div class="chart" :style="chartStyle"> <div class="chart-fill" /> <div class="chart-line" /> <div v-for="(_, i) in data" :key="i" :class="'chart-dot-' + (i + 1)" /> </div> </template> <style scoped> .chart-fill, .chart-line { transition: clip-path 0.3s ease; } </style> 

Vue accepts a CSS text string on :style for custom properties in modern builds; if your toolchain expects an object, split the same way as the React parseCssText helper.

Plain JS / live metrics

const host = document.querySelector(".chart"); async function tick() { const series = await api.latestWindow(); // length N host.style.cssText = linearVars(series); } setInterval(tick, 2000); 

With the transition CSS above, each tick morphs the area/line without tearing down nodes.

Multi-series

Each series needs its own host (same rule as static st-core):

<div class="series-a" style="/* --st-p* for A */"> <div class="fill-a"></div> <div class="line-a"></div> </div> <div class="series-b" style="/* --st-p* for B */"> <div class="fill-b"></div> <div class="line-b"></div> </div> 
elA.style.cssText = linearVars(seriesA); elB.style.cssText = linearVars(seriesB); 

Spider multi-series: one polarVars(...) per host, distinct --st-accent / opacity tokens as needed.

Checklist

  1. Compile st-core mixins for a fixed N (array length).
  2. Render host + fill/line/dots (dots need real DOM nodes).
  3. Add transition: clip-path (and top / left for dots).
  4. On data change, set --st-p* or --st-sx* / --st-sy* only.
  5. Keep N stable while streaming for smooth morphs.

Why this stays lightweight

Typical chart lib st-core + framework
Chart instance + diff CSS variables on a host
Re-render path / SVG Same DOM, new custom properties
Animation tween in JS Optional CSS transition
Heavy dependency FSCSS output + your existing UI framework

Reactive frameworks already schedule updates when props or stores change. st-core simply consumes those updates as style, which is why dynamic dashboards stay small and predictable.

Further reading

  • st-core.fscss — API, linear + spider examples
  • Dual-pass strokes and clip-path bands — same variables drive both fill and line

Smooth transitions note:

transition: clip-path 0.3s ease, top 0.3s ease; (plus left for polar dots) on your chart selectors gives native, high-frame-rate motion whenever new data points stream in — no chart animation API required.

Thanks for reading