DEV Community

Cover image for How to build an analytics dashboard with FSCSS( st-core, circle-progress, and icon-mask)
FSCSS for FSCSS tutorial

Posted on with FSCSS tutorial

How to build an analytics dashboard with FSCSS( st-core, circle-progress, and icon-mask)

A full walkthrough of the admin dashboard template: pure CSS charts, a traffic pie, mask icons, and a little JS for tabs and the mobile drawer — no Chart.js, no SVG charting library.

You will use

Module Role
st-core@v2 Design tokens + revenue area/line chart
circle-progress Traffic multi-pie
icon-mask Sidebar / toolbar icons
FSCSS ≥ 1.2.3 (1.2.4+ recommended) Compile or runtime

End result: responsive analytics shell (stats, chart, pie, goals, lists, table).


Architecture

index.html     → structure only
dashboard.fscss → tokens, layout, chart, pie, icons  →  dashboard.css
dashboard.js   → drawer, profile menu, --st-pN for chart ranges
Enter fullscreen mode Exit fullscreen mode
  • FSCSS builds geometry and chrome at compile (or runtime) time.
  • JS only changes CSS variables when the user picks 7d / 30d / 90d.
  • Production: ship HTML + compiled CSS + JS (no FSCSS runtime in the browser).

1. Project files

admin-dashboard/
├── index.html
├── dashboard.fscss
├── dashboard.css    # generated
└── dashboard.js
Enter fullscreen mode Exit fullscreen mode
mkdir admin-dashboard && cd admin-dashboard
Enter fullscreen mode Exit fullscreen mode

2. FSCSS — modules, tokens, chart

Create dashboard.fscss.

Imports and roots

@import((*) from st-core@v2)
@import((*) from circle-progress)
@import((*) from icon-mask)

@st-root()
@progress-root()
@icon-pack()
Enter fullscreen mode Exit fullscreen mode

Pulls in chart helpers, pie helpers, and icon utilities, then installs default CSS variables from each module.

Theme overrides

:root {
  --st-bg:        #0b0c10;
  --st-surface:   #12131a;
  --st-card:      #181924;
  --st-accent:    #22c55e;
  --st-text:      #f1f5f9;
  --st-muted:     #94a3b8;
  --st-border:    rgba(255,255,255,0.08);
  --st-radius-lg: 16px;

  --progress-size:   140px;
  --progress-stroke: 16px;

  @icon-color(#94a3b8)
  @icon-size(20px)
}
Enter fullscreen mode Exit fullscreen mode

Green accent, dark surfaces. Icon defaults apply to .icon masks from icon-mask.

Minimal reset + shell

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  min-height: 100vh;
  background: var(--st-bg);
  color: var(--st-text);
  font-family: system-ui, sans-serif;
  display: flex;
}

.sidebar {
  width: 240px;
  background: var(--st-surface);
  border-right: 1px solid var(--st-border);
  padding: 24px 16px;
  position: sticky;
  top: 0;
  height: 100vh;
}

.main {
  flex: 1;
  padding: 24px 28px;
  overflow-y: auto;
}

.card {
  background: var(--st-card);
  border: 1px solid var(--st-border);
  border-radius: var(--st-radius-lg);
  padding: 22px;
}
Enter fullscreen mode Exit fullscreen mode

Flex body: sidebar + main. Cards use st-core-style tokens.

Revenue chart (st-core@v2)

@arr data7d[42, 58, 65, 60, 78, 70, 92]

@st-chart-fill(.rev-fill, data7d)
@st-chart-line(.rev-line, data7d)
@st-chart-grid(.rev-grid, 5, 7)

.chart {
  @st-chart-points(data7d)
  position: relative;
  height: 220px;
}

.rev-fill {
  --st-accent: #22c55e;
  opacity: 0.22;
  transition: clip-path 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}

.rev-line {
  --st-accent: #22c55e;
  @st-chart-line-width(2.5px)
  transition: clip-path 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}
Enter fullscreen mode Exit fullscreen mode

What this does

  1. @arr data7d[…] — compile-time length (7 stops).
  2. @st-chart-points — writes --st-p1…7 as inverted Y (num(100 - v)%).
  3. Fill + line — clip-path polygons from those variables.
  4. JS can later overwrite --st-pN; CSS transitions the shape.

Markup for the chart:

<div class="chart" id="revenueChart">
  <div class="rev-fill"></div>
  <div class="rev-line"></div>
  <div class="rev-grid"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Traffic pie (circle-progress)

@circle-pie-multi(.traffic-pie)

.traffic-pie {
  --progress-size: 140px;
  --pie-a: 45;
  --pie-b: 30;
  --pie-c: 15;
  --pie-d: 10;
  --progress-color-arc: #22c55e;
  --progress-arc-2: #3b82f6;
  --progress-arc-3: #8b5cf6;
  --progress-arc-4: #f59e0b;
  position: relative;
}
Enter fullscreen mode Exit fullscreen mode

Four slices from CSS variables. Optional hole with a pseudo-element so a center label can sit on top.

Icons (icon-mask)

.logo .icon { @icon-color(#22c55e) @icon-size(24px) }
.nav a.active .icon { @icon-color(#22c55e) }
Enter fullscreen mode Exit fullscreen mode

In HTML, icons are empty spans with classes the pack understands:

<span class="icon icon-home"></span>
<span class="icon icon-chart"></span>
Enter fullscreen mode Exit fullscreen mode

Responsive sidebar

@media (max-width: 1100px) {
  .menu-btn { display: grid; }
  .sidebar {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 100;
    transform: translateX(-100%);
    transition: transform 0.25s ease;
  }
  .sidebar.open { transform: translateX(0); }
  .grid { grid-template-columns: 1fr; }
}
Enter fullscreen mode Exit fullscreen mode

JS toggles .open on the sidebar and backdrop.


3. HTML — structure only

Skeleton of index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Analytics Overview</title>

  <!-- Dev / preview: runtime -->
  <script src="https://cdn.jsdelivr.net/npm/fscss@1.2.4/runtime.min.js" async></script>
  <link type="text/fscss" href="dashboard.fscss">

  <!-- Production after CLI:
  <link rel="stylesheet" href="dashboard.css">
  -->
</head>
<body>
  <div class="sidebar-backdrop" id="sidebarBackdrop"></div>

  <aside class="sidebar" id="sidebar">
    <div class="logo">
      <span class="icon icon-chart"></span> Figsh
    </div>
    <nav class="nav">
      <a href="#" class="active"><span class="icon icon-home"></span> Dashboard</a>
      <a href="#"><span class="icon icon-chart"></span> Analytics</a>
      <!-- more links -->
    </nav>
  </aside>

  <main class="main">
    <div class="topbar">
      <button class="menu-btn" id="menuBtn" type="button" aria-label="Open menu">
        <span class="icon icon-tasks"></span>
      </button>
      <h1>Analytics Overview</h1>
      <!-- search, notifications, profile -->
    </div>

    <div class="stats">
      <div class="stat-card">
        <div class="label">Total Sales</div>
        <div class="value">$48,290</div>
        <div class="delta up">↑ 12.5%</div>
      </div>
      <!-- more stats -->
    </div>

    <div class="grid">
      <div class="card">
        <div class="tabs">
          <button class="tab active" data-range="7d" type="button">7d</button>
          <button class="tab" data-range="30d" type="button">30d</button>
          <button class="tab" data-range="90d" type="button">90d</button>
        </div>
        <div class="chart" id="revenueChart">
          <div class="rev-fill"></div>
          <div class="rev-line"></div>
          <div class="rev-grid"></div>
        </div>
        <div class="x-labels" id="xLabels"><!-- filled by JS --></div>
      </div>

      <div class="card">
        <div class="traffic-pie">
          <div class="pie-center"><strong>100</strong><br><span>Total</span></div>
        </div>
        <!-- legend -->
      </div>
    </div>
  </main>

  <script src="dashboard.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

No chart config objects in HTML — only classes and a few ids for JS.


4. JavaScript — behavior + data → variables

dashboard.js does three jobs.

Mobile drawer

const menuBtn = document.getElementById('menuBtn');
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('sidebarBackdrop');

function openSidebar() {
  sidebar.classList.add('open');
  backdrop.classList.add('open');
}
function closeSidebar() {
  sidebar.classList.remove('open');
  backdrop.classList.remove('open');
}

menuBtn?.addEventListener('click', () => {
  sidebar.classList.contains('open') ? closeSidebar() : openSidebar();
});
backdrop?.addEventListener('click', closeSidebar);
Enter fullscreen mode Exit fullscreen mode

Optional: swipe-to-close and edge-swipe-to-open on touch devices (same idea as the full template).

Chart ranges → --st-pN

const datasets = {
  '7d': {
    values: [42, 58, 65, 60, 78, 70, 92],
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  },
  '30d': {
    values: [35, 48, 55, 62, 58, 70, 75, 68, 80, 85],
    labels: ['W1', 'W2', /* … */],
  },
  // '90d': …
};

function updateChart(range) {
  const data = datasets[range];
  const style = data.values
    .map((v, i) => `--st-p${i + 1}: ${100 - v}%;`)
    .join(' ');

  document.getElementById('revenueChart').style.cssText = style;
  document.getElementById('xLabels').innerHTML =
    data.labels.map((l) => `<span>${l}</span>`).join('');
}

document.querySelectorAll('.tab').forEach((tab) => {
  tab.addEventListener('click', () => {
    document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
    tab.classList.add('active');
    updateChart(tab.dataset.range);
  });
});

updateChart('7d');
Enter fullscreen mode Exit fullscreen mode

Why 100 - v?

CSS 0% is the top of the box. A high score should sit high on the chart, so human 90 becomes --st-p*: 10%.

Length note: polygon stop count is fixed when you compile from @arr data7d (7 points). Extra --st-p8… for longer series need a longer compile-time array (or a max-N template). Tabs still update values and labels; see st-core docs for fixed vs bounded length.

Profile dropdown

const profileBtn = document.getElementById('profileBtn');
const dropdown = document.getElementById('profileDropdown');

profileBtn?.addEventListener('click', (e) => {
  e.stopPropagation();
  dropdown.classList.toggle('open');
});
document.addEventListener('click', () => dropdown?.classList.remove('open'));
Enter fullscreen mode Exit fullscreen mode

5. Compile for production

npm install -g fscss@1.2.4
fscss dashboard.fscss dashboard.css
Enter fullscreen mode Exit fullscreen mode

In index.html:

<link rel="stylesheet" href="dashboard.css">
<!-- remove runtime script and type="text/fscss" -->
Enter fullscreen mode Exit fullscreen mode

CI option: GitHub Action on templates/**/*.fscss that runs the same command and commits dashboard.css for static hosting.


6. Mental model

Layer Responsibility
FSCSS modules Tokens, chart polygons, pie, icons
Your .fscss Layout, theme, wire modules to classes
HTML Regions and class names
JS UX + map number[] → --st-pN

You are not driving a chart API. You are updating design tokens that already-drawn CSS shapes read.


7. Next steps

  • Swap green tokens for your brand.
  • Point datasets at a real API.
  • Reuse the same CSS in Svelte/React by setting style / cssText from props (see st-core integration/svelte).
  • Add React/Next wrappers later; the compile step stays the same.

Template & preview

Clone the template folder, run with runtime for a quick look, then compile to CSS for production.

Build the chrome and charts in FSCSS. Keep HTML boring. Let JS only move the numbers.

Top comments (0)