DEV Community

Cover image for Yes, pure CSS can do radar charts — and st-core.fscss still stays lightweight
FSCSS for FSCSS tutorial

Posted on with FSCSS tutorial

Yes, pure CSS can do radar charts — and st-core.fscss still stays lightweight

The short answer to “there are no pure CSS chart libraries that can generate radar/spider charts” is: there is one now. And it did not get heavy to do it.

st-core.fscss v2 draws area, line, and spider/radar charts with the same idea:

  • FSCSS @arr for data
  • CSS custom properties for coordinates
  • clip-path: polygon() for shapes
  • Gradients for grid / spokes

No SVG. No canvas. No Chart.js / D3 / Recharts on the critical path.

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

Requires FSCSS ≥ 1.2.3 (spider helpers ship in v2).


The claim this answers

For a long time the pragmatic take was:

Pure CSS can fake a bar or a simple line. Radar needs polar math, multiple series, spokes, labels — so you need SVG or a library.

That was fair when “CSS charts” meant hand-written polygons or one-off hacks.

st-core@v2 treats radar the same way it treats linear charts: data in → variables out → polygons on screen. Polar is just a different coordinate writer.


Is it still lightweight?

Yes.

st-core is not a monolithic stylesheet you dump on every page.

Design rule What it means
What you call is what is invoked Helpers only emit CSS when you call them (@st-spider-fill, @st-spider-line, …). Unused chart families stay out of the output.
No third-party chart runtime No vendor JS, no unused Chart.js themes, no SVG path builder.
Selective import Import only the helpers you need, or compile once with the CLI and ship plain CSS.
Tokens, not frameworks Color, fill opacity, stroke scale are CSS variables (--st-accent, --st-spider-fill-opacity, …).

In FSCSS terms: @import registers @define helpers. Nothing paints until you call them with a selector. Call spider only → you do not pay for a linear-only dashboard you never used. Call fill + line only → dots and grid stay out.

Compile path (production):

npm install -g fscss@1.2.6
fscss chart.fscss chart.css
Enter fullscreen mode Exit fullscreen mode

Ship chart.css. No client preprocessor. No chart library bundle.


Minimal radar (one series)

<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.6/runtime.min.js" async></script>
<style>
@import((*) from st-core@v2)
@st-root()
@st-spider-root()

@arr skills[80, 65, 90, 55, 70, 85]

@st-spider-fill(.spider-fill, skills)
@st-spider-line(.spider-line, skills)
@st-spider-dots(.spider-dot-, skills, 9px)
@st-spider-grid(.spider-grid)
@st-spider-spokes(.spider-spokes, skills)

.spider {
  @st-spider-points(skills, 42)
  position: relative;
  width: 280px;
  height: 280px;
  border-radius: 50%;
  background: var(--st-surface);
}
</style>

<div class="spider">
  <div class="spider-grid"></div>
  <div class="spider-spokes"></div>
  <div class="spider-fill"></div>
  <div class="spider-line"></div>
  <div class="spider-dot-1"></div>
  <div class="spider-dot-2"></div>
  <div class="spider-dot-3"></div>
  <div class="spider-dot-4"></div>
  <div class="spider-dot-5"></div>
  <div class="spider-dot-6"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Six axes, fill, dual-pass stroke, dots, rings, spokes — still only the helpers you named.


How polar stays pure CSS

One rule (same as linear charts):

Points write. Renderers read.

Helper Role
@st-spider-points(array, rmax) On the host: writes --st-sxN / --st-syN via sin / cos in calc()
@st-spider-fill / @st-spider-line / @st-spider-dots Read those variables; emit clip-path / positioned dots
@st-spider-grid Concentric rings (radial gradient)
@st-spider-spokes / @st-spider-spokes-n Radial axes

Angle 0° is at the top. Radius is a % of the box (rmax, often 42).

/* conceptual output per vertex */
--st-sx1: calc(50% + (r * 1%) * sin(0deg));
--st-sy1: calc(50% - (r * 1%) * cos(0deg));
Enter fullscreen mode Exit fullscreen mode

Polygon vertex count is fixed at compile time from @arr length. Changing values at runtime is free (set the variables). Changing length needs a matching compiled shape (same constraint as linear series).


Multi-series without coordinate clashes

Each series gets its own host and its own @st-spider-points scope:

@arr teamA[92, 78, 85, 55, 48, 80]
@arr teamB[68, 82, 70, 95, 62, 58]

@st-spider-fill(.fill-a, teamA)
@st-spider-line(.line-a, teamA)
@st-spider-fill(.fill-b, teamB)
@st-spider-line(.line-b, teamB)

.spider-wrap.team-a {
  @st-spider-points(teamA, 42)
  --st-accent: #9d7eff;
  --st-spider-fill-opacity: 28%;
}
.spider-wrap.team-b {
  @st-spider-points(teamB, 42)
  --st-accent: #4fffb0;
  --st-spider-fill-opacity: 30%;
}
Enter fullscreen mode Exit fullscreen mode

Shared grid/spokes once; series only differ by tokens and points. Still no chart library.


Live updates (optional JS)

You do not need JS to paint the chart. You only need it if values change after load:

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(' ');
}

document.querySelector('.spider').style.cssText =
  polarVars([80, 65, 90, 55, 70, 85]);
Enter fullscreen mode Exit fullscreen mode

That is a few lines of state — not a charting framework.


Why “lightweight” holds after radar

Radar did not force a second architecture:

Before (linear) After (spider)
@arr + --st-pN @arr + --st-sxN / --st-syN
clip-path fill / line Same, polar vertices
Call only the helpers you need Same
Compile to plain CSS Same

You did not import “st-core full product suite.” You invoked spider helpers. Unused linear-only pages never see those rules if you selective-import or compile a dedicated sheet.

No unused third-party stylesheets. No hidden JS chart theme. No hydration tax for a static skills matrix.


When pure CSS radar is the right tool

Good fit

  • Skills matrices, multi-axis scores, team comparison cards
  • Dashboards that already avoid heavy front-end frameworks
  • SEO / first paint: real DOM, no canvas blank frame
  • Design-system tokens already driving the rest of the UI

Less ideal

  • Thousands of points with continuous streaming redraw (canvas wins)
  • Interaction-heavy brushing/zoom (libraries still win)
  • Dynamic axis count every second (compile-time polygon length)

st-core is honest about that boundary: visualization as styling, not as a full analytics runtime.


Try it

<!--- prototype --->
<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.6/runtime.min.js" async></script>
Enter fullscreen mode Exit fullscreen mode
# production
npm i -g fscss
fscss radar.fscss radar.css
Enter fullscreen mode Exit fullscreen mode

Resources/examples : st-core.fscss README — Spider / Radar

Visual builder: hub.devtem.org/st-core.fscss/visual-builder

Related:


Radar is no longer proof that pure CSS charts “can’t.” With st-core@v2 it is the same lightweight contract as area and line — what you call is what is invoked, tokens instead of a library, compile when you want zero runtime. MIT · fscss-ttr.

Top comments (0)