This is a follow-up to my earlier post (Building a trading dashboard UI with zero JavaScript — just st-core.fscss.)If you haven't read that one, the quick summary: st-core is an FSCSS plugin that lets you declare chart infrastructure — fills, lines, dots, axes, grids — entirely at the CSS layer. No canvas, no chart library.
That post showed you the static side. This one shows you how to make it live.
The "Aha" Moment
The thing that unlocks st-core for real data viz is understanding what the chart actually is under the hood.
The chart shape IS a clip-path polygon.
When st-core compiles your FSCSS, it generates CSS that reads from a set of custom properties — --st-p1 through --st-p8 — and uses them to construct the polygon coordinates. That's it. So updating the chart at runtime is just:
element.style.cssText = `--st-p1: 40%; --st-p2: 75%; --st-p3: 55%; ...`;
One line. No DOM surgery. No re-render cycle. The CSS transition handles the rest.
The Demo: Expenses Chart
Here's the full chart we're building — an analytics card with a live-updating area chart, a stat card, and a peak-tracking dot.
CodePen demo
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/npm/fscss@1.1.24/exec.min.js" async></script>
<style>
@import((*) from st-core) @st-root() @st-container(body) .wrapper {
flex-direction: column;
gap: 16px;
width: 100%;
}
@st-chart-fill(.chart-fill)
@st-chart-line(.chart-line)
@st-chart-dot(.chart-dot, 70, 60)
.chart {
width: 100%;
height: 200px;
border-radius: 20px;
@st-chart-points(20, 25, 21, 37, 30, 60, 27, 50)
}
@st-stat-card(.stat-card)
@st-chart-axis-x(.x-axis)
@st-chart-axis-y(.y-axis)
@st-chart-grid(.chart-grid, 10, 7)
.chart-fill, .chart-line {
height: 100%;
transition: clip-path .9s cubic-bezier(.4, 0, .2, 1);
}
</style>
</head>
<body>
<div class="wrapper">
<div class="stat-card">
<div class="st-stat-label">TOTAL EXPENSES</div>
<div class="st-stat-value">$1,326.03</div>
<div class="st-stat-delta up">+5.1% vs last week</div>
</div>
<div class="chart">
<div class="chart-fill"></div>
<div class="chart-line"></div>
<div class="chart-dot"><span class="tooltip">$405.67</span></div>
<div class="chart-grid"></div>
<div class="y-axis">
<span>0</span><span>10</span><span>20</span><span>30</span>
<span>40</span><span>50</span><span>60</span><span>70</span>
</div>
</div>
<div class="x-axis days">
<span>Sun</span><span>Mon</span><span>Tue</span><span>Wed</span>
<span>Thu</span><span>Fri</span><span>Sat</span><span>Sun</span>
</div>
</div>
</body>
</html>
Directive Breakdown
The chart block
@st-chart-fill(.chart-fill)
@st-chart-line(.chart-line)
@st-chart-dot(.chart-dot, 70, 60)
.chart {
@st-chart-points(20, 25, 21, 37, 30, 60, 27, 50)
}
| Directive | What it generates |
|---|---|
@st-chart-fill |
Area fill via clip-path polygon, reads --st-p1..8
|
@st-chart-line |
Stroke-only overlay of the same shape |
@st-chart-dot |
Absolute-positioned peak marker, initial position (70, 60)
|
@st-chart-points |
Compiles the 8 values into --st-p1 through --st-p8
|
Support directives
@st-stat-card(.stat-card) /* label / value / delta slots */
@st-chart-axis-x(.x-axis) /* wires x-axis label container */
@st-chart-axis-y(.y-axis) /* wires y-axis label container */
@st-chart-grid(.chart-grid, 10, 7) /* 10 rows × 7 cols grid */
The JS Bridge (19 lines)
This is the entire runtime layer. No chart library, no framework.
const chartFill = document.querySelector(".chart-fill");
const chartLine = document.querySelector(".chart-line");
const chartDot = document.querySelector(".chart-dot");
// st-core clip-path is top-down, so flip the value
function normalize(n) {
return (100 - n) + '%';
}
// X positions for the 8 columns
const xStops = ['0%', '14%', '28%', '42%', '57%', '71%', '85%', '100%'];
function update(points) {
// Set --st-p1..8 directly on the elements
const vars = points.map((v, i) => `--st-p${i + 1}: ${normalize(v)};`).join(' ');
chartLine.style.cssText = vars;
chartFill.style.cssText = vars;
// Move dot to the peak
const highest = Math.max(...points);
const idx = points.indexOf(highest);
chartDot.style.top = normalize(highest);
chartDot.style.left = xStops[idx];
chartDot.querySelector('.tooltip').textContent = `$${(highest * 5.5).toFixed(2)}`;
}
Call update() with any array of 8 numbers and the chart animates there. That's the whole bridge.
Connecting to Real Data
WebSocket (e.g. financial feed)
const socket = new WebSocket('wss://your-api.com/expenses/stream');
socket.onmessage = (event) => {
const { daily } = JSON.parse(event.data);
// daily is an array of 8 values, one per day
update(daily);
};
REST polling (e.g. every 30 seconds)
async function poll() {
const res = await fetch('/api/expenses/weekly');
const data = await res.json();
update(data.points); // [20, 45, 33, 60, 27, 51, 38, 44]
}
poll();
setInterval(poll, 30_000);
In both cases your only job is feeding an array of 8 numbers into update(). The CSS does everything else — shape, fill, transition, dot position.
Why clip-path Makes This Work
st-core uses clip-path: polygon() which means:
- The shape is arbitrary — true area curves, not stepped bars
-
Transitions are compositor-accelerated —
clip-pathanimates on the GPU, not the main thread -
No repaints — changing
--st-p*variables doesn't touch the DOM structure at all
The transition declaration is doing a lot of heavy lifting here:
.chart-fill, .chart-line {
transition: clip-path .9s cubic-bezier(.4, 0, .2, 1);
}
That single line is what makes the chart feel like a live dashboard instead of a static image that gets replaced.
The "Gravy Path": Compiling to Pure CSS
The exec.min.js CDN script is for prototyping and demos. For production — and especially for large-scale projects — you compile your .fscss file down to totally pure, standard CSS.
Two ways to do this:
1. VS Code Extension
Install the FSCSS extension from the VS Code marketplace, and install fscss from npm. It watches your .fscss files and compiles on save — you get instant output CSS right next to your source file. This is the fastest feedback loop for building UIs.
2. FSCSS npm package
npm install -g fscss
fscss "path/to/style.fscss" "path/to/style.css"
More details on that fscss.devtem.org.
The compiled output is 100% standard CSS. No runtime dependency, no JavaScript required for the styles. You ship the .css file and it just works.
Why this matters for large-scale projects
When you're managing a dashboard with dozens of chart components, keeping your chart configuration in .fscss files is a huge advantage:
/* charts/expenses.fscss */
@st-chart-points(20, 25, 21, 37, 30, 60, 27, 50)
/* charts/revenue.fscss */
@st-chart-points(44, 51, 60, 72, 65, 80, 78, 90)
You get a single source of truth for chart shapes, baseline datasets, theming, and responsive behavior — all in .fscss. The compiled CSS is what ships to users. The JS layer stays tiny because it only handles the live variable updates.
The Full Stack, One More Time
.fscss source
└── compiled via npm
└── pure CSS output
├── clip-path polygon (shape)
├── --st-p1..8 variables (data)
├── transitions (motion)
└── stat card / axis / grid layout
Runtime JS (optional, for live data)
└── update(points[]) → sets --st-p1..8 on elements
└── CSS transition takes over → smooth animation
The mental model: FSCSS owns the structure and shape. JS owns the numbers. CSS owns the motion.
The core pattern doesn't change for any of these. You're still just feeding --st-p* variables and letting the compiled CSS handle the rest.
Top comments (0)