DEV Community

Cover image for Pure CSS Charts? Yes, Really. Introducing st-core.fscss v2
FSCSS for FSCSS tutorial

Posted on

Pure CSS Charts? Yes, Really. Introducing st-core.fscss v2

If you've ever pulled in heavy libraries just to render a simple line chart, you know the drill: a few hundred KB of JavaScript, a canvas or SVG element, and a hydration step before anything even shows up on screen. For a lot of dashboards, that's massive overkill.

st-core.fscss takes a different approach. It's a CSS-only visualization system built on FSCSS that renders area charts, multi-line charts, and stat dashboards using nothing but clip-path: polygon() and CSS custom properties. No canvas. No SVG. No chart library shipping down the wire.

Here's how it works, and how to build real charts with it.

The Core Idea

Every chart in st-core is really just a polygon. Give the mixin an array of data points, and it:

  1. Spaces the points evenly across the chart width (0% to 100%, based on array length)
  2. Normalizes each value into a Y-position
  3. Generates a clip-path: polygon() that traces the line (or fills the area beneath it)
@arr myData[50, 10, 97, 35, 66, 50, 80, 54, 70, 60]

@st-chart-fill(.chart-fill, myData)
@st-chart-line(.chart-line, myData)
Enter fullscreen mode Exit fullscreen mode

That's the whole rendering pipeline. The array can be any length: 5 points, 50 points, doesn't matter.

Getting It Running

For Testing - Drop the runtime in your <head>:

<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.3/runtime.min.js" async></script>
Enter fullscreen mode Exit fullscreen mode

Then import the module inside your <style> block:

@import((*) from st-core@v2)
Enter fullscreen mode Exit fullscreen mode

Prefer a build step for production? The CLI compiles .fscss file straight to production CSS:

npm install -g fscss@latest
fscss input.fscss output.css
Enter fullscreen mode Exit fullscreen mode

Vscode Extention for syntax highlights and auto-compile: FSCSS Support


A Basic Area Chart

<style>
@import((*) from st-core@v2)

@st-root()

@arr myData[20, 45, 28, 80, 65, 90, 40]

@st-chart-fill(.chart-fill, myData)
@st-chart-line(.chart-line, myData)

.chart {
  @st-chart-points(myData)
  position: relative;
  height: 200px;
  width: 100%;
  max-width: 400px;
  background: var(--st-surface);
  border-radius: 16px;
}
</style>

<div class="chart">
  <div class="chart-fill"></div>
  <div class="chart-line"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Two things worth calling out about how this splits responsibilities:

  • @st-chart-fill / @st-chart-line declare the renderer for a selector. The array argument just tells it how many points to expect.
  • @st-chart-points(myData) is what actually writes the --st-p1 through --st-p{n} values onto an element. Any child that doesn't call it inherits from the nearest ancestor that did — which is exactly what makes the multi-series case below work cleanly.

Multi-Line, Multi-Area Charts

This is where the point-inheritance model pays off. Each series gets its own renderer and its own explicit @st-chart-points call, so nothing bleeds into another series by accident:

@arr seriesA[30, 50, 75, 40, 85, 60]
@arr seriesB[10, 25, 45, 20, 55, 30]

@st-chart-fill(.fill-a, seriesA)
@st-chart-line(.line-a, seriesA)

@st-chart-fill(.fill-b, seriesB)
@st-chart-line(.line-b, seriesB)

.chart {
  @st-chart-points(seriesA)
  position: relative;
  height: 220px;
  width: 100%;
  background: var(--st-bg);
}

.fill-a {
  @st-chart-points(seriesA)
  opacity: 0.6;
  --st-accent: #9d7eff;
}
.line-a {
  @st-chart-points(seriesA)
  --st-accent: #9d7eff;
}

/* .fill-b / .line-b MUST set their own points, otherwise they'd
   silently inherit .chart's seriesA values */
.fill-b {
  @st-chart-points(seriesB)
  opacity: 0.3;
  --st-accent: #4fffb0;
}
.line-b {
  @st-chart-points(seriesB)
  --st-accent: #4fffb0;
}
Enter fullscreen mode Exit fullscreen mode
<div class="chart">
  <div class="chart-fill fill-a"></div>
  <div class="chart-line line-a"></div>

  <div class="chart-fill fill-b"></div>
  <div class="chart-line line-b"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Stack as many series as you want this way. Each one is independently opaque, colored, and pointed at its own array.

Custom Data Points and Markers

For a single highlighted marker (a peak, a tooltip anchor), use @st-chart-dot directly:

/* selector, x%, y%, size */
@st-chart-dot(.chart-dot, 70, 60, 12px)
Enter fullscreen mode Exit fullscreen mode

To mark every point in a dataset automatically:

/* Generates .dot-1, .dot-2 ... .dot-N */
@st-chart-dots(.dot-, myData, 8px)
Enter fullscreen mode Exit fullscreen mode

Combine that with @st-chart-grid(.chart-grid, rows, cols) and the axis wrappers @st-chart-axis-x / @st-chart-axis-y, and you've got a fully annotated chart with zero JS.

Making It Interactive Anyway

Pure CSS rendering doesn't mean the data has to be static. Updating a chart at runtime is just a matter of rewriting the --st-p{n} variables:

const chartLine = document.querySelector(".chart-line");
const normalize = (n) => (100 - n) + '%';

function updatePoints(pointsArray) {
  const cssVars = pointsArray
    .map((v, i) => `--st-p${i + 1}: ${normalize(v)};`)
    .join(' ');

  chartLine.style.cssText = cssVars;
}

updatePoints([50, 20, 85, 40, 95]);
Enter fullscreen mode Exit fullscreen mode

Pair that with a transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1); on .chart-fill / .chart-line, and dataset changes animate smoothly through the browser's compositor, without touching layout or paint.

Why This Matters

  • Zero hydration. The chart is visible the instant CSS parses. No JS has to boot before you see data.
  • Tiny footprint. The compiled output is shapes and custom properties, around 0.8kb minified.
  • GPU-accelerated updates. Because everything animates through clip-path and CSS variables, transitions run on the compositor thread instead of the main thread.

If you're building something where every kilobyte of JS matters (embedded widgets, low-power devices, SEO-sensitive pages), st-core gets you real, responsive charts without the usual charting-library tax.


st-core.fscss is MIT licensed and built on FSCSS

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

Top comments (0)