G Gentelella v4.0.0

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:

  1. No framework, no SPA. Vanilla ES2022 with <script type="module">. No React, Vue, or Svelte. No state management. No virtual DOM.
  2. One shell, every page. Sidebar, topbar, and footer render from a single NAV array. Pages declare which item is active via body attributes.
  3. One source of truth for design. Every color, radius, and shadow lives in _tokens.scss as 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 a NavItem.key in the NAV array. 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:

  1. Reads data-shell, data-page, data-breadcrumb from the <body> tag
  2. Calls renderShell() from shell-render.js (a pure string function — no DOM, no browser APIs)
  3. Inserts the sidebar before <main>, the topbar inside it, the footer after it
  4. 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 #1ABB9C in a partial, stop — use var(--primary) and add a token if needed.
  • active / done / on for state classes. Toggled by JS via classList.
  • data- attributes drive JS, not ids (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, addEventListener directly.
  • 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.jsIntl.DateTimeFormat and 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.