Architecture
Gentelella v4 is a vanilla-JavaScript multi-page Vite app with a shared shell, token-driven SCSS, and lazy-loaded heavy libraries. No framework, no SPA, no state manager. Here's how it fits together.
Last updated May 22, 2026
Gentelella v4 was rewritten in 2026 around three constraints:
- No framework, no SPA. Vanilla ES2022 with
<script type="module">. No React, Vue, or Svelte. No state management. No virtual DOM. - One shell, every page. Sidebar, topbar, and footer render from a single
NAVarray. Pages declare which item is active via body attributes. - One source of truth for design. Every color, radius, and shadow lives in
_tokens.scssas a CSS custom property. Change one variable, everything restyles — including charts.
This page walks through what that looks like in practice.
File layout
gentelella/
├── production/ # 58 HTML pages, one per route
│ ├── index.html # Operations dashboard
│ ├── index2.html # Analytics dashboard
│ ├── inbox.html
│ ├── kanban.html
│ ├── theme.html # Live theme generator
│ ├── playground.html # Component playground
│ ├── offline.html # Service-worker fallback
│ └── ... # See the NAV array for the full list
├── src/
│ ├── main-v4.js # Entry — same on every page
│ └── v4/ # All runtime JS, one module per concern
│ ├── shell.js # mountShell(), event delegation, drawer, theme toggle
│ ├── shell-render.js # NAV array + pure HTML renderers
│ ├── charts.js # ECharts factories (dashboardNetwork, revenueLine…)
│ ├── tables.js # DataTables init
│ ├── inbox.js # Inbox client
│ ├── kanban.js # Drag-drop board
│ ├── calendar.js # Month-grid calendar
│ ├── file-manager.js # Tree + grid file browser
│ ├── command-palette.js # ⌘K modal
│ ├── modal.js / toast.js / menus.js
│ └── ...
│ └── scss/
│ └── v4/
│ ├── main.scss # Aggregator — @use's the partials below
│ ├── _tokens.scss # CSS custom properties (THE source of truth)
│ ├── _layout.scss # Sidebar, topbar, main, grid, footer
│ ├── _components.scss
│ ├── _forms.scss
│ ├── _widgets.scss
│ ├── _pages.scss
│ ├── _apps.scss # Inbox / kanban / file manager layouts
│ ├── _datatable.scss
│ └── _auth.scss
├── public/
│ ├── sw.js # Service worker
│ ├── site.webmanifest # PWA manifest
│ └── images/
└── vite.config.js
There is no bootstrap.scss, no jquery.js, no dist/ in version control. Vite handles SCSS compilation directly — no PostCSS, no Babel, no Tailwind. Three production dependencies (ECharts, DataTables.net, Leaflet), nine dev dependencies, ~178 MB node_modules.
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.0">
<title>Operations | Gentelella 2026 v4</title>
<link rel="icon" href="../images/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script type="module" src="/src/main-v4.js"></script>
</head>
<body data-shell="admin" data-page="dashboard" data-breadcrumb="Home > Dashboards > Operations">
<main class="main">
<div class="page-wrapper">
<!-- Page-specific content -->
</div>
</main>
</body>
</html>
Three pieces matter:
data-shell="admin"— opt-in. Pages with this attribute get the sidebar, topbar, and footer injected by the Vite plugin at build time. Pages without it (auth, error pages, landing) skip the shell entirely.data-page="dashboard"— string key matching aNavItem.keyin theNAVarray. The matching sidebar link gets.active.data-breadcrumb="Home > Dashboards > Operations"—>-separated string. The topbar renders it as breadcrumbs; the last segment becomes the current page.
The <main class="main"> + <div class="page-wrapper"> wrapper is required — the SCSS layout assumes that structure.
The shell is injected at build time
This is the key trick. Most static templates render the sidebar at runtime, which causes a flash of unstyled content (FOUC) while JS boots. Gentelella’s shellInjectionPlugin in vite.config.js runs during Vite’s transformIndexHtml hook:
- Reads
data-shell,data-page,data-breadcrumbfrom the<body>tag - Calls
renderShell()fromshell-render.js(a pure string function — no DOM, no browser APIs) - Inserts the sidebar before
<main>, the topbar inside it, the footer after it - Also injects: PWA manifest link, theme-color meta, pre-paint theme script, OG/Twitter SEO tags, skip-to-content link
The HTML you ship already contains the full shell DOM. When JS loads, mountShell() detects the pre-rendered shell, skips re-rendering, and just attaches event handlers — no flicker, no layout shift.
The early-paint theme script
Generated automatically by the Vite plugin for every page:
<script>
(function () {
try {
var t = localStorage.getItem('theme');
var d = window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = t || (d ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
} catch (e) {}
})();
</script>
The block runs synchronously before CSS arrives, sets data-theme="dark" on <html> for dark-mode users, and CSS variables under :root[data-theme="dark"] activate immediately. No flash.
Lazy-loaded heavy libraries
main-v4.js is ~5 KB gzipped. The expensive stuff loads only when a page actually needs it:
// Inbox client — only on /inbox.html
if (document.getElementById('inbox-root')) {
import('./v4/inbox.js').then((m) => m.initInbox());
}
// Calendar — only on pages with a calendar grid
if (document.querySelector('.calendar-grid')) {
import('./v4/calendar.js').then((m) => m.initCalendar());
}
// Settings page persistence
if (document.querySelector('.settings-content')) {
import('./v4/settings.js').then((m) => m.initSettings());
}
// Advanced form controls (date range, rich text, multi-select)
if (document.querySelector('[data-date-range], [data-rich-text], [data-multi-select]')) {
import('./v4/form-controls.js').then((m) => m.initFormControls());
}
ECharts is the heaviest dependency at ~400 KB. initCharts() checks for [data-chart] elements first and returns early if none — pages without charts never download the library. The same pattern applies to DataTables (only on pages with [data-datatable]), Leaflet (only production/map.html), and the inbox / kanban / calendar modules.
Event delegation, not direct binding
The whole template wires interactivity through delegated click handlers on document, filtered by .closest():
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.toggle');
if (toggle) toggle.classList.toggle('on');
});
document.addEventListener('click', (e) => {
const cb = e.target.closest('.todo-cb');
if (!cb) return;
cb.classList.toggle('done');
cb.closest('.todo-row')?.classList.toggle('done');
});
Two benefits: zero per-element listeners (the template has hundreds of toggles, badges, todo rows), and DOM swaps survive — adding a new toggle dynamically doesn’t need a separate .bind() call.
How charts, tables, and maps stay theme-aware
The integrations don’t know about CSS variables — they accept colors as JS values. Each module reads tokens fresh from getComputedStyle at render time:
const tokens = () => {
const cs = getComputedStyle(document.documentElement);
return {
primary: cs.getPropertyValue('--primary').trim(),
primaryDk: cs.getPropertyValue('--primary-dk').trim(),
text: cs.getPropertyValue('--text').trim(),
textMuted: cs.getPropertyValue('--text-muted').trim(),
borderLight: cs.getPropertyValue('--border-color-light').trim(),
bgSurface: cs.getPropertyValue('--bg-surface').trim(),
// ...
};
};
When the theme toggle flips data-theme, the chart module’s resize-aware re-render path picks up the new token values automatically. Same pattern for DataTables (re-skinned in _datatable.scss using token variables) and Leaflet.
Conventions
A few patterns that show up everywhere:
- CSS variables, never hex. If you find yourself writing
#1ABB9Cin a partial, stop — usevar(--primary)and add a token if needed. active/done/onfor state classes. Toggled by JS viaclassList.data-attributes drive JS, notids (except for stable mount points like#inbox-root).- Page templates — every page in
production/follows the same skeleton. Copy any existing page as a starting point. - No inline styles for design decisions. Bespoke padding on one card is fine inline; never put colors or tokens inline.
- No DOM utilities library.
querySelector,classList,addEventListenerdirectly. - Don’t track
dist/. Gitignored, rebuilt by Vite.
What’s deliberately not there
Things you’d expect in a typical Bootstrap-era admin template that v4 removed:
- Bootstrap 5 — replaced with custom UI primitives in
_components.scss - jQuery — replaced with vanilla DOM APIs
- Popper.js — dropdowns use plain CSS positioning
- Moment / Day.js —
Intl.DateTimeFormatand the calendar widget’s own formatters - Perfect Scrollbar — native browser scrollbars styled via CSS
node_modules dropped from ~600 MB on the old Gentelella to ~178 MB. Production dependencies dropped from ~30 to 3 (echarts, datatables.net, leaflet).
See Migrating from older Gentelella on the upstream changelog for the full removal list and v4 design rationale.