st-core.fscss draws charts with pure CSS, clip-path polygons plus custom properties, no JS chart library, no SVG, no canvas. Svelte doesn't need to draw anything for it. It just needs to own the data and reactively set --st-p1, --st-p2, and so on whenever your series changes. Wanted to write up the actual integration because getting the responsibilities split correctly took me a minute the first time through.
Why compiled mode, not the runtime CDN
Svelte already has a build pipeline. Shipping the FSCSS runtime to a Svelte app just to expand a few chart mixins into static CSS is unnecessary work for the browser to do on every load. The runtime CDN script is fine for a CodePen or a quick static HTML demo, but for an actual Svelte or SvelteKit app you compile once at build time and import plain CSS.
Three layers, each with one job:
| Layer | Job |
|---|---|
| FSCSS CLI | Expand @st-chart-fill / @st-chart-line into static CSS |
| Svelte | Reactive data[] → --st-pN
|
| Browser CSS | Paint, plus optional clip-path transitions |
Svelte state → style="--st-p1: 30%; --st-p2: 45%; …"
↓
compiled st-core.css
↓
chart on screen
1. Write the FSCSS entry file
src/styles/st-core.fscss:
@import((*) from st-core@v2)
@st-root()
/* Length at compile time = number of polygon stops */
@arr chartData[0, 0, 0, 0, 0, 0, 0]
@st-chart-fill(.chart-fill, chartData)
@st-chart-line(.chart-line, chartData)
.chart {
@st-chart-points(chartData)
position: relative;
width: 100%;
height: 220px;
background: var(--st-surface);
border-radius: var(--st-radius-lg);
overflow: hidden;
}
.chart-fill,
.chart-line {
transition: clip-path 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
The zeros are placeholders, they only matter for their count. Svelte overwrites the real Y values through CSS variables at runtime. What you can't change without recompiling is the array's length — match it to your series (7 days of data → 7 entries).
2. Compile it as part of your build
npm install -D fscss@latest
npx fscss src/styles/st-core.fscss src/styles/st-core.css
Wire it into package.json so it runs automatically before every build:
{
"scripts": {
"build:st-core": "fscss src/styles/st-core.fscss src/styles/st-core.css",
"prebuild": "npm run build:st-core"
}
}
Then import the compiled CSS once, wherever makes sense for your app (root layout, app.css, or the chart's parent component):
import '../styles/st-core.css';
3. Map your data to --st-pN
Chart geometry in st-core is top-down, the same way CSS itself is. A high value needs to sit near 0%, not 100%. So every point gets inverted:
--st-pᵢ = (100 - humanValue) %
A score of 90 ends up 10% from the top, which is exactly where you'd expect it visually.
Here's a reusable Chart.svelte that does that mapping reactively, Svelte 4 style:
<script>
/** @type {number[]} 0–100, higher = taller */
export let data = [];
export let height = '200px';
export let color = '#9d7eff';
$: cssVars = [
...data.map((value, index) => `--st-p${index + 1}: ${100 - value}%`),
`--st-accent: ${color}`,
`height: ${height}`
].join('; ');
</script>
<div class="chart" style={cssVars}>
<div class="chart-fill"></div>
<div class="chart-line"></div>
</div>
And the Svelte 5 version, using $props / $derived instead of the old export-let / reactive-statement pattern:
<script>
let { data = [], height = '200px', color = '#9d7eff' } = $props();
let cssVars = $derived(
[
...data.map((value, index) => `--st-p${index + 1}: ${100 - value}%`),
`--st-accent: ${color}`,
`height: ${height}`
].join('; ')
);
</script>
<div class="chart" style={cssVars}>
<div class="chart-fill"></div>
<div class="chart-line"></div>
</div>
Both do the exact same job, just with whichever reactivity API your Svelte version uses.
4. Use it like any other component
<script>
import './styles/st-core.css';
import Chart from './lib/Chart.svelte';
let weekly = [45, 70, 30, 90, 60, 80, 50];
function randomize() {
weekly = Array.from({ length: 7 }, () => Math.floor(Math.random() * 80) + 10);
}
</script>
<Chart data={weekly} color="#4fffb0" height="250px" />
<!-- Svelte 4 -->
<button type="button" on:click={randomize}>Randomize</button>
<!-- Svelte 5 -->
<!-- <button type="button" onclick={randomize}>Randomize</button> -->
Reassigning weekly recomputes cssVars, the browser applies the new custom properties, and the compiled CSS's clip-path transition animates the shape change. No chart library re-rendering, no SVG path recalculation, no manual DOM diffing on your end.
Fixed length vs changing length
Because geometry is baked in at compile time, the array length is the one thing you can't change on the fly:
| Situation | What to do |
|---|---|
| Always exactly N points | Compile @arr chartData with N zeros, always pass exactly N values from Svelte |
| Length varies, up to a max of M | Compile M zeros, then data.slice(0, M) and pad shorter series with 0
|
If you compile a template expecting 5 points and hand it 8 values from Svelte, the extra --st-p6, --st-p7, --st-p8 have nowhere to go, there's no polygon stop for them until you recompile with a longer array.
What not to do in production
<!-- Fine for a demo, avoid as your only path in a real Svelte app -->
<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.3/runtime.min.js"></script>
Runtime mode is for CodePen and quick static HTML, not for a build pipeline that already exists. See the plain HTML integration sample if that's what you actually want.
The mental model, condensed
-
@st-chart-points, fill, and line define the structure and only ever read--st-p*. - Svelte's only job is to replace those variables when the data changes.
-
transition: clip-pathgives you motion for free, no animation library needed.
Nothing here is Svelte-specific past step 3, the same pattern maps directly to Vue, React, or vanilla JS, only the reactivity layer changes. Contributions for other frameworks welcome under integration/.
Repo: github.com/fscss-ttr/st-core.fscss
Svelte sample: integration/svelte
Compile the chart. React to the data. Let CSS paint.
Top comments (0)