If you have ever built an AI product UI, you know the pattern. A sidebar, a chat column, a metrics panel, a streaming caret, some usage cards. It is a lot of CSS before you write a single line of product logic.
In this tutorial we build an AI Ops Console using four FSCSS modules, compile everything to one plain CSS file, and wire it up with a small vanilla JavaScript layer. No frameworks. No runtime preprocessor in the browser.
What we are building
An AI Ops Console with a desktop layout (sidebar, chat, metrics panel) and a mobile layout (hamburger sidebar drawer, bottom sheet metrics panel at 960px and below).
| Surface | Module |
|---|---|
| App shell, sidebar, messages, prompt, stream caret, thinking dots, usage cards, workflow nodes, tool chips, citations | ai-ui.fscss |
| Token burn area and line chart (clip-path, no SVG or canvas) | st-core@v2 |
| GPU, Queue and Quota rings | circle-progress |
| Team avatar stack | overlapui |
| Mock agent replies, live chart nudges, mobile drawers | dashboard.js |
The project is split into four files:
ai-dashboard/ ├── dashboard.html # markup only ├── dashboard.fscss # all FSCSS + layout ├── dashboard.css # compiled output (generated) └── dashboard.js # chat, drawers, chart updates You compile once and ship plain CSS.
Prerequisites
- Node.js 18 or newer (for the FSCSS CLI)
- A modern browser
- Optional: VS Code with the FSCSS Support extension
Install the compiler globally:
npm install -g fscss@1.2.6 You need FSCSS 1.2.6 or newer. ai-ui requires it, and st-core needs 1.2.3 or newer.
1. Project setup
mkdir ai-dashboard && cd ai-dashboard touch dashboard.html dashboard.fscss dashboard.js dashboard.css will be generated by the CLI in step 3.
2. dashboard.fscss: the styles source
This file imports the four modules, calls their helpers against your own class names, defines the chart series, and holds all layout and responsive rules.
/* dashboard.fscss AI Ops Dashboard: FSCSS source Compile: fscss dashboard.fscss dashboard.css */ /* Modules (registry names; CLI + runtime resolve these) */ @import((*) from ai-ui) @import((*) from st-core@v2) @import((*) from circle-progress) @import((*) from overlapui) /* Theme + design tokens */ @ai-compose-dashboard() /* ai-ui tokens + dashboard theme */ @st-root() /* st-core chart tokens */ @progress-root() /* circle-progress tokens */ @ou-root() /* overlapui tokens */ /* ai-ui structure helpers */ @ai-shell(.ai-shell) @ai-sidebar(.ai-sidebar) @ai-main(.ai-main) @ai-messages(.ai-messages) @ai-message(.ai-message) @ai-message-user(.ai-message-user) @ai-message-assistant(.ai-message-assistant) @ai-bubble(.ai-bubble) @ai-avatar(.ai-avatar) @ai-prompt(.ai-prompt) @ai-prompt-box(.ai-prompt-box) @ai-prompt-send(.ai-prompt-send) @ai-stream(.ai-stream) @ai-thinking(.ai-thinking) @ai-usage(.ai-usage) @ai-stat(.ai-stat) @ai-workflow(.ai-workflow) @ai-node(.ai-node) @ai-edge(.ai-edge) @ai-tools(.ai-tools) @ai-tool-chip(.ai-tool-chip) @ai-card(.ai-card) @ai-divider(.ai-divider) @ai-citations(.ai-citations) @ai-citation(.ai-citation) /* st-core linear chart */ @arr tokenSeries[32, 48, 55, 42, 68, 75, 61, 88, 72, 91, 84, 96] @st-chart-fill(.chart-fill, tokenSeries) @st-chart-line(.chart-line, tokenSeries) .token-chart { @st-chart-points(tokenSeries) position: relative; height: 160px; width: 100%; background: var(--st-surface, #161422); border-radius: 12px; overflow: hidden; } /* circle-progress */ @circle-progress(.cp) .cp-72 { @progress-range(72) } .cp-48 { @progress-range(48) } .cp-91 { @progress-range(91) } .cp { --progress-size: 110px; --progress-stroke: 10px; --progress-color-bg: transparent; --progress-color-track: rgba(255, 255, 255, 0.08); --progress-color-arc: var(--ai-accent, #e879f9); --progress-color-glow: color-mix(in srgb, var(--ai-accent, #e879f9) 40%, transparent); --progress-color-value: var(--ai-text, #f5f3ff); --progress-color-label: var(--ai-text-muted, #a78bfa); font-size: 0.95rem; font-weight: 600; } /* overlapui avatars */ @avatar-overlap(.team-faces) .team-faces { --ou-avatar-size: 36px; --ou-overlap: -12px; margin-block: 8px; } /* Base layout */ * { box-sizing: border-box; margin: 0; padding: 0; } html, body { height: 100%; font-family: var(--ai-font, system-ui, -apple-system, sans-serif); background: var(--ai-bg, #0c0a12); color: var(--ai-text, #f5f3ff); -webkit-text-size-adjust: 100%; } .ai-shell { min-height: 100vh; min-height: 100dvh; height: 100vh; height: 100dvh; display: grid; grid-template-columns: var(--ai-sidebar-w, 240px) 1fr; } .ai-sidebar { display: flex; flex-direction: column; gap: 16px; padding: 20px 16px; overflow-y: auto; border-right: 1px solid var(--ai-border, rgba(255, 255, 255, 0.08)); background: var(--ai-bg, #0c0a12); z-index: 40; } .brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 1.05rem; letter-spacing: -0.02em; } .brand-mark { width: 28px; height: 28px; border-radius: 8px; background: linear-gradient(135deg, var(--ai-accent, #e879f9), #818cf8); display: grid; place-items: center; font-size: 0.75rem; font-weight: 800; color: #0c0a12; flex-shrink: 0; } .nav { display: flex; flex-direction: column; gap: 4px; } .nav a { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 8px; color: var(--ai-text-muted, #a78bfa); text-decoration: none; font-size: 0.875rem; transition: background 0.15s, color 0.15s; } .nav a:hover, .nav a.active { background: color-mix(in srgb, var(--ai-accent, #e879f9) 12%, transparent); color: var(--ai-text, #f5f3ff); } .side-section { display: flex; flex-direction: column; gap: 8px; } .side-section h3 { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ai-text-muted, #a78bfa); font-weight: 600; } .cp-row { display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap; } .cp-item { display: flex; flex-direction: column; align-items: center; gap: 4px; font-size: 0.7rem; color: var(--ai-text-muted, #a78bfa); } .ai-main { display: flex; flex-direction: column; min-height: 0; min-width: 0; overflow: hidden; } .topbar { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--ai-border, rgba(255, 255, 255, 0.08)); gap: 10px; flex-wrap: wrap; flex-shrink: 0; } .topbar h1 { font-size: 1rem; font-weight: 600; } .topbar-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .chip { font-size: 0.75rem; padding: 4px 10px; border-radius: 999px; background: color-mix(in srgb, var(--ai-accent, #e879f9) 15%, transparent); color: var(--ai-accent, #e879f9); border: 1px solid color-mix(in srgb, var(--ai-accent, #e879f9) 30%, transparent); white-space: nowrap; } .workspace { flex: 1; display: grid; grid-template-columns: 1fr minmax(260px, 320px); min-height: 0; overflow: hidden; } .chat-col { display: flex; flex-direction: column; min-height: 0; min-width: 0; border-right: 1px solid var(--ai-border, rgba(255, 255, 255, 0.06)); } .ai-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 14px; -webkit-overflow-scrolling: touch; } .ai-prompt { padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--ai-border, rgba(255, 255, 255, 0.06)); flex-shrink: 0; } .right-panel { display: flex; flex-direction: column; gap: 16px; padding: 16px; overflow-y: auto; background: color-mix(in srgb, var(--ai-surface, #12101a) 80%, transparent); -webkit-overflow-scrolling: touch; } .panel-card { background: var(--ai-card, #1a1625); border: 1px solid var(--ai-border, rgba(255, 255, 255, 0.08)); border-radius: 12px; padding: 14px; } .panel-card h3 { font-size: 0.8rem; font-weight: 600; margin-bottom: 10px; color: var(--ai-text-muted, #a78bfa); } .ai-workflow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 0.75rem; } .ai-node { padding: 6px 10px; border-radius: 6px; background: var(--ai-surface, #161422); border: 1px solid var(--ai-border, rgba(255, 255, 255, 0.1)); } .ai-node.is-active { border-color: var(--ai-accent, #e879f9); color: var(--ai-accent, #e879f9); box-shadow: 0 0 0 1px color-mix(in srgb, var(--ai-accent, #e879f9) 40%, transparent); } .ai-edge { width: 16px; height: 2px; background: var(--ai-border, rgba(255, 255, 255, 0.2)); } .ai-usage { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .ai-stat { background: var(--ai-surface, #161422); border-radius: 10px; padding: 10px 12px; } .ai-stat-label { display: block; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ai-text-muted, #a78bfa); margin-bottom: 4px; } .ai-stat-value { font-size: 1.15rem; font-weight: 700; letter-spacing: -0.02em; } .ai-stat-delta { font-size: 0.7rem; margin-left: 6px; } .ai-stat-delta.up { color: #4fffb0; } .ai-stat-delta.down { color: #ff5e7d; } .ai-tools { display: flex; flex-wrap: wrap; gap: 6px; } .ai-tool-chip { font-size: 0.7rem; padding: 4px 8px; border-radius: 6px; background: var(--ai-surface, #161422); border: 1px solid var(--ai-border, rgba(255, 255, 255, 0.1)); } .ai-tool-chip.is-done { border-color: #4fffb0; color: #4fffb0; } .ai-tool-chip.is-running { border-color: var(--ai-accent, #e879f9); color: var(--ai-accent, #e879f9); } .muted { color: var(--ai-text-muted, #a78bfa); font-size: 0.8rem; } /* Mobile controls (hidden on desktop) */ .menu-btn, .panel-btn, .sidebar-backdrop { display: none; } /* TABLET <= 960px */ @media (max-width: 960px) { .ai-shell { grid-template-columns: 1fr; } .ai-sidebar { position: fixed; inset: 0 auto 0 0; width: min(280px, 86vw); transform: translateX(-105%); transition: transform 0.25s ease; box-shadow: none; border-right: 1px solid var(--ai-border, rgba(255, 255, 255, 0.08)); } body.sidebar-open .ai-sidebar { transform: translateX(0); box-shadow: 8px 0 32px rgba(0, 0, 0, 0.45); } .sidebar-backdrop { display: block; position: Source:https://dev.to/fscss/build-an-ai-ops-dashboard-with-fscss-ai-ui-st-core-circle-progress-overlapui-59m5Related posts

Your RAG Finds Some of the Answer. I Tested 5 Chunking Strategies From This Month's Papers to See Who Finds All of It.
Goravel routing, route groups and controllers
Find and Replace in the Browser: Regex, Case Sensitivity and a Result You Can Check
