DEV Community

Cover image for How to Use st-core.fscss with Svelte (Compiled)
FSCSS for FSCSS tutorial

Posted on

How to Use st-core.fscss with Svelte (Compiled)

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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) %
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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> -->
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

  1. @st-chart-points, fill, and line define the structure and only ever read --st-p*.
  2. Svelte's only job is to replace those variables when the data changes.
  3. transition: clip-path gives 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)