Source Code

<div class="container-fluid py-4">
  <div class="row bei-shell mx-auto g-0 border rounded-3 overflow-hidden">
    <div class="col-3 border-end bg-light p-3" id="beiFolders">
      <div class="list-group list-group-flush">
        <button class="list-group-item list-group-item-action active d-flex justify-content-between align-items-center" data-folder="inbox">
          Inbox <span class="badge text-bg-dark rounded-pill" id="beiInboxBadge"></span>
        </button>
        <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-folder="sent">
          Sent <span class="badge text-bg-dark rounded-pill" id="beiSentBadge"></span>
        </button>
        <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-folder="drafts">
          Drafts <span class="badge text-bg-dark rounded-pill" id="beiDraftsBadge"></span>
        </button>
      </div>
    </div>

    <div class="col-4 border-end p-0 bei-list-col" id="beiList"></div>

    <div class="col-5 p-4" id="beiReadingPane">
      <p class="text-muted">Select an email to read it here.</p>
    </div>
  </div>
</div>

Bootstrap Email Inbox Layout — Free HTML CSS Snippet

Bootstrap Email Inbox Layout · Layouts · Plain HTML, CSS & JS · Live preview

What's included

Features

Real Bootstrap 5.3 three-column row layout with list-group sidebar and rounded-pill badges
A single DATA object drives the folder badges, message list, and reading pane consistently
Unread counts computed live per folder via Array.filter rather than hardcoded badge numbers
Clicking a message marks it read and updates its folder badge in the same interaction
Bold-vs-normal subject styling driven by a CSS class, not inline per-row styles
Selected message stays visually highlighted in the list independent of read/unread state
Reading pane preserves multi-line email bodies via white-space: pre-line
Folder switching fully resets the list and reading pane to avoid showing stale content

About this UI Snippet

Bootstrap Email Inbox Layout — HTML, CSS & JavaScript

Screenshot of the Bootstrap Email Inbox Layout snippet rendered live

An inbox UI is really three views onto the same data: a folder count, a message list, and a reading pane, and this snippet keeps all three perfectly in sync by driving them from a single DATA object rather than three independently maintained pieces of markup. DATA has one array per folder (inbox, sent, drafts), and each email object carries from, subject, preview, body, and an unread boolean — that boolean is the entire state machine behind the bold/unbold behavior and the badge counts.

The layout itself is a real Bootstrap row split into three columns with g-0 (no gutter, so the borders between columns sit flush): a col-3 folder sidebar built from a real list-group, a col-4 scrollable message list, and a col-5 reading pane. Each folder button carries a rounded-pill badge text-bg-dark; updateBadges() recomputes each folder's unread count with DATA[folder].filter(e => e.unread).length and writes it into the matching badge, using the empty string instead of 0 when a folder has no unread mail so an empty badge pill does not render as a hollow dot.

Switching folders (clicking Inbox/Sent/Drafts) toggles Bootstrap's active class between the sidebar buttons, updates currentFolder, resets selectedId to null, resets the reading pane's placeholder text, and calls renderList() — which clears and rebuilds #beiList from DATA[currentFolder] alone, so switching folders can never show stale messages from a previous folder. Each list row conditionally gets a .bei-unread class, which is what makes the subject line font-weight: 700 in CSS — a plain CSS rule, not per-row inline styling — and a .bei-selected class if its id matches selectedId, so the currently open message stays visibly highlighted in the list even after other state changes trigger a re-render.

The read behavior lives in selectEmail(id): it looks up the email by id in the current folder's array, and if it was unread, flips unread to false and calls updateBadges() immediately — the non-obvious detail here is the order of operations: the unread flag is mutated on the underlying data object *before* renderList() runs, so the subject immediately loses its bold weight in the same render pass that shows the row as selected, rather than needing a second click or a delayed update. The reading pane is then filled with the subject, sender, and body, using white-space: pre-line in the inline style so the \n characters embedded in each email's body string render as real line breaks instead of collapsing into one line.

Because every view is a pure function of DATA, currentFolder, and selectedId, this structure ports directly to React/Vue/Angular state management — the three variables become state, and renderList/reading-pane markup become derived JSX or template output instead of manual innerHTML writes.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI coding assistant like Claude to add a search box that filters the visible message list by subject or sender, or to add a "mark all as read" button per folder. It's also worth asking it to add swipe-to-archive gestures for a mobile-friendly version.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a three-column Bootstrap 5.3 email inbox layout using the real Bootstrap CDN (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble Bootstrap.

Requirements:
- A folder sidebar (Inbox, Sent, Drafts) built from a real Bootstrap list-group, each folder showing a rounded-pill badge with its live unread-message count.
- A middle message list for the currently selected folder, where unread messages render with a bold subject line.
- Clicking a message in the list marks it as read (removing the bold, decrementing that folder's unread badge) and displays its full content in a right-hand reading pane.
- Switching folders swaps the message list entirely and resets the reading pane, without losing read/unread state when you switch back.
- Drive all three views (badges, list, reading pane) from one shared in-memory data structure so they always stay consistent with each other.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Load the snippetThe Inbox folder is selected by default, showing four messages, three of them bold (unread), and a "Select an email" placeholder in the reading pane.
  2. 2
    Click a bold (unread) messageIt immediately loses its bold weight in the list, the Inbox badge count decreases by one, and the full email body appears in the right-hand reading pane.
  3. 3
    Click a message you already readIt opens in the reading pane again without changing any badge count, since it was already marked read.
  4. 4
    Switch to the Sent folderThe message list swaps entirely to sent messages, the reading pane resets to the placeholder text, and the sidebar highlights Sent as active.
  5. 5
    Switch back to InboxThe previously read messages remain unbold and the badge count reflects only the mail still unread, proving the read state persisted across folder switches.

Real-world uses

Common Use Cases

Webmail and messaging clients
The canonical three-pane email layout used by nearly every desktop webmail client.
CHAT
Support ticket inboxes
Adapt the same folder-list-reading-pane pattern for a support ticket queue, similar to how a table row selection list highlights the active record.
Learning single-source-of-truth UI
A clear example of driving multiple views (badges, list, pane) from one shared data object instead of duplicated state.
DASHBOARD
Notification center panels
Reuse the unread-badge and click-to-read pattern inside an admin dashboard sidebar notification drawer.
CRM activity and message logs
Pair with a profile card follow view so selecting a contact could show their message history in the same reading-pane pattern.

Got questions?

Frequently Asked Questions

Yes — selectEmail() checks the email's unread flag, sets it to false if true, and immediately calls updateBadges() and renderList(), so the bold subject and the folder's unread badge count both update in the same click.

No — the unread flag is mutated directly on the email object inside the shared DATA structure, not on a copy, so it persists correctly when you navigate away to another folder and back to Inbox.

Yes — move DATA into component state grouped by folder, derive unread counts with a memoized selector or computed property, and replace the innerHTML-based renderList() with JSX/template list rendering bound to the same currentFolder and selectedId state, updating them in useEffect/onMounted only for any initial data fetch.

Yes — the list-group, badge, and row/col grid classes are purely presentational; replace them with Tailwind's flex/grid utilities on the same markup, since all the read/unread and selection logic operates on plain classes and data, not on Bootstrap-specific behavior.

updateBadges() writes unreadCount(folder) || '' into each badge's textContent, so a zero count evaluates to the empty string and the badge pill renders visually empty rather than showing a literal zero, matching how most real inbox UIs handle a fully-read folder.

Replace the DATA object with data fetched from your backend on load (keeping the same {id, from, subject, preview, body, unread} shape per email, grouped by folder key), and call renderList() and updateBadges() once the fetch resolves.