Architecture
Adminator is built around a single JS shell that renders all 18 pages from one NAV manifest, and a token-driven CSS-variable design system with light and dark variants. Here's how the pieces fit together.
Last updated May 21, 2026
The 2026 redesign rewrote Adminator around three constraints:
- One bundle. Every page loads the same JS and the same CSS. Pages are HTML files that fill three placeholder divs.
- One source of truth for navigation. A
NAVarray inShell.jsdrives the sidebar, the breadcrumbs, and the ⌘K command palette. - One source of truth for styling. Every color, shadow, radius, and font size lives in
_tokens.scssas a CSS variable with light + dark variants. Touch one variable, every component updates.
This page walks through what that looks like in practice.
File layout
src/
├── *.html # 18 pages — each ~500 lines, mostly content
├── assets/
│ ├── scripts/2026/ # The only JS
│ │ ├── index.js # entry — imports SCSS, mounts shell, runs init
│ │ ├── Shell.js # NAV manifest + sidebar/topbar/footer renderers
│ │ ├── init.js # theme toggle, dropdowns, nav-groups, accordions
│ │ ├── charts.js # Chart.js seeds + tokens()
│ │ ├── calendar.js # FullCalendar seed events + toolbar binding
│ │ └── maps.js # jsvectormap world map
│ └── styles/2026/ # The only SCSS
│ ├── index.scss # entry — imports all partials below
│ ├── _tokens.scss # CSS variables, light + dark
│ ├── _base.scss # reset, body, .eyebrow, .mono
│ ├── _shell.scss # .shell, sidebar, topbar, footer chrome
│ ├── _components.scss # .hero, .btn, .card, .grid, .table, .tag
│ ├── _forms.scss # inputs, select, textarea, check, switch
│ ├── _ui.scss # alerts, badges, progress, tabs, accordion, modal
│ ├── _charts.scss / _calendar.scss / _fullcalendar.scss
│ ├── _dashboard.scss / _email.scss / _chat.scss / _data.scss
│ ├── _auth.scss / _error.scss
│ └── _responsive.scss # all media queries in one place
There is no Bootstrap.scss, no jquery.js, no legacy admin.js. The whole runtime is six JS files (~47 KB unminified) plus 18 SCSS partials. Webpack splits the output into runtime.js, 2026.js, plus auto-split vendor-fullcalendar.js / vendor-chartjs.js / vendors.js for the three heavy libraries.
Page anatomy
Every shell page is the same skeleton:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Adminator · Dashboard</title>
<script>
// Early-paint theme bootstrap — sets data-theme before CSS arrives,
// so dark-mode users don't see a light flash.
(function () {
try {
var saved = localStorage.getItem('dash26-theme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', saved || (prefersDark ? 'dark' : 'light'));
} catch (e) {
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script>
</head>
<body data-active="dashboard" data-crumbs="Workspace | Dashboard">
<div class="shell">
<div data-shell-sidebar></div>
<div class="main">
<div data-shell-topbar></div>
<main class="content">
<!-- Page-specific content goes here -->
</main>
<div data-shell-footer></div>
</div>
</div>
</body>
</html>
Three pieces matter:
data-activematches akeyin theNAVmanifest (e.g.dashboard,email,calendar). The shell renderer marks the corresponding sidebar item withis-active.data-crumbsis a|-separated list. The topbar renders it as breadcrumbs, with the last segment highlighted as the current page.- Three placeholder divs —
data-shell-sidebar,data-shell-topbar,data-shell-footer— get replaced at runtime byShell.js.
Standalone pages (sign-in, sign-up, 404, 500) skip the .shell wrapper and use their own layout (.auth-shell, .error-shell).
The early-paint theme script
That <script> block in <head> is doing real work. Without it, a dark-mode user sees a flash of light styling between the HTML parse and the first stylesheet load — known as a FOUC (flash of unstyled content).
The script reads localStorage.dash26-theme (if previously set) or falls back to prefers-color-scheme, then sets data-theme on <html> before CSS arrives. CSS variables under :root[data-theme="dark"] activate immediately. No flash.
This is one of the few places the template embeds JS inline rather than in the bundle — because the bundle hasn’t loaded yet when this runs.
The shell renderer
Shell.js exports a single NAV array — that’s the source of truth for the sidebar, breadcrumbs, and command palette. A simplified entry looks like:
export const NAV = [
{
label: 'Workspace',
items: [
{ key: 'dashboard', text: 'Dashboard', href: 'index.html',
icon: '<path d="M3 12 12 3l9 9"/><path d="M5 10v10h14V10"/>' },
],
},
{
label: 'Components',
items: [
{ key: 'pages', text: 'Pages',
icon: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>',
children: [
{ key: 'blank', text: 'Blank', href: 'blank.html' },
{ key: '404', text: '404', href: '404.html' },
],
},
],
},
];
Items with children render as expandable nav groups. Icons are inline SVG paths (24×24 viewBox, stroked) so they inherit currentColor and re-color on theme change without any extra work.
The mountShell() function finds the three placeholder divs and replaces each with the rendered HTML.
How charts and maps stay in sync with the theme
Chart.js, FullCalendar, and jsvectormap don’t know about your CSS variables — they accept colors as JS values. The 2026 template solves this with a small tokens() function in each integration module:
function tokens() {
const cs = getComputedStyle(document.documentElement);
return {
primary: cs.getPropertyValue('--primary').trim(),
success: cs.getPropertyValue('--success').trim(),
danger: cs.getPropertyValue('--danger').trim(),
text: cs.getPropertyValue('--t-base').trim(),
border: cs.getPropertyValue('--border').trim(),
// ...
};
}
When the user toggles the theme, a MutationObserver watching documentElement for data-theme changes fires the rebuild — charts.js destroys its Chart instances and re-instantiates them with the new tokens. Same for FullCalendar and the vector map.
The user sees an instant theme swap. Charts redraw in the new color scheme without a page reload.
Conventions
A few patterns that show up everywhere in the codebase:
- CSS variables, never hex. All colors live in
_tokens.scss. If you find yourself writing#2563EBin a partial, stop — usevar(--primary)and add the token if it doesn’t exist. is-active/is-open/is-donefor state classes (BEM-ish modifiers). Toggled by JS.data-attributes drive JS, neverids except forthemeToggleandheroDate.- Inline
<svg>icons (24×24 viewBox,stroke-width: 1.75–2,fill: none). Avoid icon-font dependencies. 'Inter'for body,'Inter Tight'for display,'JetBrains Mono'for numerics/eyebrows. Loaded once via@import url(...)at the top ofindex.scss.
What’s not in the bundle
Things you’d expect in a typical jQuery-era admin template that Adminator 4 deliberately removed:
- Bootstrap — replaced with custom UI primitives in
_components.scss,_ui.scss,_forms.scss - jQuery — replaced with
querySelectorAll,classList,addEventListener - Popper.js — dropdown positioning is plain CSS
position: absolute - Day.js — date formatting uses native
Intl.DateTimeFormat - Perfect Scrollbar — native browser scrollbars styled via CSS
Bundle size dropped from ~4.5 MB (v3) to ~700 KB (v4) — a 85% reduction. See the migration guide for what changed.