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
- 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
mkdir admin-dashboard && cd admin-dashboard
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()
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)
}
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;
}
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);
}
What this does
-
@arr data7d[…]— compile-time length (7 stops). -
@st-chart-points— writes--st-p1…7as inverted Y (num(100 - v)%). - Fill + line —
clip-pathpolygons from those variables. - 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>
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;
}
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) }
In HTML, icons are empty spans with classes the pack understands:
<span class="icon icon-home"></span>
<span class="icon icon-chart"></span>
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; }
}
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>
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);
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');
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'));
5. Compile for production
npm install -g fscss@1.2.4
fscss dashboard.fscss dashboard.css
In index.html:
<link rel="stylesheet" href="dashboard.css">
<!-- remove runtime script and type="text/fscss" -->
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
datasetsat a real API. - Reuse the same CSS in Svelte/React by setting
style/cssTextfrom props (see st-coreintegration/svelte). - Add React/Next wrappers later; the compile step stays the same.
Template & preview
| Live preview | hub.devtem.org/st-core.fscss/templates/admin-dashboard/ |
| Template source | github.com/fscss-ttr/st-core.fscss/tree/main/templates/admin-dashboard |
| st-core | github.com/fscss-ttr/st-core.fscss |
| circle-progress | github.com/Figsh/Circle-progress.fscss |
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)