G Gentelella v4.0.0

Inbox

Gentelella's inbox is a fully interactive mail client built into the template — folders, reader pane, compose modal, J/K/R/S/# keyboard shortcuts, search. Self-contained: data, state, and UI all live in src/v4/inbox.js. The host page only provides an empty mount point.

Last updated May 22, 2026

The inbox at production/inbox.html is not a screenshot — it’s a working mail client with about 30 seed messages, real folder logic, search, keyboard shortcuts, compose/reply/forward modals, and per-folder unread counts that update live as you act.

The page itself is minimal:

<body data-shell="admin" data-page="inbox" data-breadcrumb="Home > Apps > Inbox">
  <main class="main">
    <div class="page-wrapper">
      <div id="inbox-root"></div>
    </div>
  </main>
</body>

Everything else is rendered by src/v4/inbox.js, lazy-loaded only when #inbox-root exists on the page.

What’s wired

FeatureHow
5 folders (Inbox, Sent, Drafts, Starred, Trash) + 4 labelsviewFilter() switches the visible message set
Click a messageOpens it in the reader pane, marks as read, updates folder counts
Star toggleClick the star icon — moves the message in/out of Starred
Archive (trash)Move to Trash. From Trash: Restore or Delete forever
ComposeModal with To / Subject / Body fields — submits to Sent (or saves to Drafts if subject is blank)
Reply / ForwardOpen compose pre-filled
SearchFilters by subject + body + sender, scoped to the active folder
Per-folder unread countsPill on each folder updates on every action
Keyboard shortcutsJ/K to navigate, R to reply, S to star, # to trash, C to compose

State model

A single in-memory array of messages:

const messages = seed; // 30+ entries

// Each message:
{
  id: 'msg-1',                          // unique
  folder: 'inbox' | 'sent' | 'drafts' | 'starred' | 'trash',
  unread: boolean,
  starred: boolean,
  label: 'work' | 'personal' | 'newsletter' | 'travel' | '',
  from: 'Sarah K.',                     // display name
  fromEmail: '[email protected]',
  subject: 'Re: Q1 design review',
  preview: "I've added comments to the figma file…",   // for the list view
  body: 'Hey,\n\nI\'ve added comments…',                // full body
  time: '9:42 AM',
}

Mutations update the array in place, then call renderAll() to repaint. No state library, no immutability — the simplicity is the feature. Total state surface is small enough that the whole inbox repaints in under a frame on every action.

Keyboard shortcuts

The handlers are bound once on init and scoped to the inbox page via if (!document.getElementById('inbox-root')) return;. Inputs and textareas are also skipped (if (e.target.matches('input, textarea')) return;) so the shortcuts don’t fire while you’re typing in compose or search:

KeyAction
J / Next message — auto-opens it in the reader
K / Previous message — auto-opens it in the reader
RReply to the selected message (opens compose pre-filled). No-op on Drafts.
SToggle star on the selected message
#Move to Trash (or delete forever if already in Trash)
CCompose new message

Navigation auto-opens the message in the reader as you move — there’s no separate “Enter to open” step. Gmail-flavored but not Gmail-identical.

Folder logic

viewFilter() switches what the list view shows:

function viewFilter(msg) {
  switch (currentView.kind) {
    case 'folder':
      // 'starred' is virtual — any starred message regardless of folder, but not Trash
      if (currentView.value === 'starred') return msg.starred && msg.folder !== 'trash';
      return msg.folder === currentView.value;
    case 'label':
      // Labels exclude Trash too
      return msg.label === currentView.value && msg.folder !== 'trash';
  }
}

Two things worth noting:

  • Starred is a view, not a folder. A message starred while in Inbox stays in Inbox; selecting “Starred” in the sidebar just filters to show it.
  • Trash is opaque to labels. Trashed messages don’t show in label views — when something’s trashed, it’s invisible everywhere except Trash itself.

The search input filters the active folder/label, not globally:

function searchFilter(msg) {
  if (!searchQuery) return true;
  const q = searchQuery.toLowerCase();
  return msg.subject.toLowerCase().includes(q)
      || msg.body.toLowerCase().includes(q)
      || msg.from.toLowerCase().includes(q);
}

Combined with viewFilter(), this means “search Inbox”, “search Sent”, “search Starred” — never “search everything across folders”. That’s the same behavior as Gmail’s per-label search. If you need cross-folder search, fork visibleMessages() to ignore viewFilter when there’s a search query.

Customizing the seed data

The seed messages live at the top of inbox.js:

const m = (p) => ({ id: id(), unread: false, starred: false, label: '', ...p });

const seed = [
  m({ folder: 'inbox', unread: true, starred: true, label: 'work',
      from: 'Sarah K.', fromEmail: '[email protected]',
      subject: 'Re: Q1 design review',
      preview: "I've added comments to the figma file…",
      body: "Hey,\n\nI've added comments…",
      time: '9:42 AM' }),
  // ...
];

m() is a constructor that supplies sensible defaults. To add your own messages, append to seed. To replace seed with API data, the template ships data-adapter.js with a list/update/create/remove API surface:

import { useApiMode, httpAdapter } from './data-adapter.js';

const adapter = useApiMode()
  ? httpAdapter('/api/messages', { listKey: 'messages' })
  : seedAdapter(seed);

// All adapters expose the same surface:
const items = await adapter.list({ folder: 'inbox' });
await adapter.update(id, { unread: 0 });
await adapter.create({ /* new message */ });
await adapter.remove(id);

useApiMode() returns true when:

  • The URL has ?api=1 (handy for live demos that switch between “static preview” and “real backend”), or
  • window.__GENTELELLA_API__ = true is set before module load (good for build-time injection)

The default is false so the template ships demo-ready without any backend dependency. The inbox already wires the adapter — look at hydrateFromApi(root) in inbox.js for the actual call pattern.

Compose, Reply, Forward

All three reuse the same modal — they just pre-fill different fields:

  • Compose — opens with empty To / Subject / Body
  • Reply — pre-fills To with fromEmail, Subject with Re: <subject>, Body with a quoted blockquote
  • Forward — pre-fills To empty, Subject with Fwd: <subject>, Body with the full original message indented

Submitting the modal:

  • If Subject is non-empty → creates a new message in sent
  • If Subject is empty → creates the message in drafts
  • Either way, the visible folder updates immediately if you’re looking at Sent/Drafts

The modal itself is generic — exported from src/v4/modal.js as showModal(opts). The inbox calls it with form contents; the same modal is used by Kanban, Settings, and Confirm dialogs across the template.

Per-message counts

Folder unread counts update on every mutation:

function unreadCountInFolder(folder) {
  return messages.filter((m) => {
    if (folder === 'starred') return m.starred && m.unread && m.folder !== 'trash';
    return m.folder === folder && m.unread;
  }).length;
}

Cheap because the message array is small. For a real app with thousands of messages, you’d want a backend-driven count and only recompute on folder-change events.

Responsive layout

The inbox uses a three-pane layout (sidebar / list / reader) on desktop. At smaller widths:

  • Below 1024px: reader collapses, list takes full width
  • Below 768px: sidebar collapses to a top bar, list/reader stack

The transitions are CSS-driven via _pages.scss. The updateBackLayout() function in inbox.js adjusts a few classes when the layout flips — mainly to handle the back button that appears in the reader header on mobile.

See also

  • Kanban — the other interactive component on the template
  • Architecture — the lazy-loading pattern that keeps the inbox out of pages that don’t need it
  • Adding a page — how to add inbox-style pages of your own