Design Lounge
Install

Nº 565 of 605

Mail folders sidebar

An email app sidebar: account row, raspberry Compose, folders with unread counts, collapsible labels with colour chips, a split storage meter and G-then-I shortcuts.

Preview

Open full size
1280 × 800 · 20.4 KB · 227 lines

Use it

Free to use · credit appreciated

1 Once, in your project · Node 22.20+

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Mail folders sidebar" (piece sidebar-mail-folders) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

224 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

The left sidebar of an email client, Kestrel, for one signed-in person. Top to bottom: an account row, a 48px raspberry Compose button with its C shortcut shown, eight system folders, a collapsible Labels section with colour chips and counts, a collapsed More section, a storage meter split into three parts, and a line that teaches ? for shortcuts. The inbox list beside it is only a hint: a 520px two-line list and an empty reading pane. The detail worth copying is the count grammar. Unread counts are bold ink and make the folder name bold too; total counts (Drafts 2, Scheduled 1, Spam 12) stay regular grey. The two mean different things, so they never look the same.

Reference behaviour

  1. Initial state: Inbox is the current folder (raspberry tint #F6E3EA, raspberry text, count “24”). Snoozed and Clients are bold because they hold unread mail. Labels is open; More is collapsed.
  2. Hover a folder: background --hover, text to --ink. No movement.
  3. Click a folder or label: it becomes current (aria-current="page"), the list header changes to its name, and the list shows its conversations, or a one-line empty state (“Trash is empty”, “12 messages in Spam. Deleted automatically after 30 days.”).
  4. Click the Labels or More heading: the chevron turns −90° and the group folds to zero height over 280ms (grid rows 1fr → 0fr). A folded group is inert, so its links leave the tab order.
  5. Click Compose or press C: the list header switches to “New message” with a short drafting hint (in a full app this opens the composer).
  6. Press G then I, S or D within 900ms to jump to Inbox, Starred or Drafts. Press / to focus search. Shortcuts are ignored while typing in a field or when a modifier is held.
  7. Press ? or click “show them”: a 260px shortcuts card rises 6px into place next to the sidebar’s bottom. Esc closes it.
  8. Below 760px the sidebar becomes a drawer: a 40px menu button in the top bar slides it in from the left over 280ms with a 40% scrim; picking a folder, tapping the scrim or Esc closes it.

Structure

1280 × 800
┌────────────── 272 ─────────┬──────────────────────────────────────────────┐
│ (NB) Nadia Brennan      ⌄  │ [ Search mail                       / ]  64  │
│ ┌ ✎ Compose ─────── [C] ┐  ├────────── 520 ──────────┬───────────────────┤
│ └───────────────────────┘  │ Inbox  24 unread        │                   │
│ ▓ Inbox               24   │ • Halden Ceramics  9:42 │                   │
│   Starred                  │   Revised quote…        │   ✉               │
│   Snoozed (bold)       3   │   Attached is v3…       │ No conversation   │
│   Sent · Drafts 2 · Sched 1│ • Ruth Okafor      9:08 │ open              │
│   Spam 12 · Trash          │   …                     │                   │
│ ⌄ LABELS              +    │                         │                   │
│   ■ Clients (bold)     6   │                         │                   │
│   ■ Invoices 2 · Travel …  │                         │                   │
│ › MORE                     │                         │                   │
│ ────────────────────────── │                         │                   │
│ 11.4 GB of 15 GB      76%  │                         │                   │
│ ▬▬▬▬▬▬▬▬▬▬▬▭▭              │                         │                   │
│ [?] for shortcuts…         │                         │                   │
└────────────────────────────┴─────────────────────────┴───────────────────┘
  • <aside class="side" aria-label="Mail folders">, a flex column, padding 12px 12px 10px.
    • .acct <button>: 32px initials disc, name 14/600, address 12px, chevron.
    • .compose <button>: 48px tall, 14px radius, pencil icon, label, <kbd>C</kbd> pushed right.
    • .scroll (flex 1, overflow-y: auto) holding <nav aria-label="Folders"> with a <ul> of folder <button class="f">s, then two .sec sections, each a .sh header (<button aria-expanded aria-controls> + optional 28px add button) over a .fold wrapper.
    • .store: label row, role="meter" bar with three segments, legend, upsell link.
    • .keys hint with <kbd>?</kbd> and a toggle button for the shortcuts card.
  • .pop role="dialog" with a <dl> of shortcuts (fixed, beside the sidebar).
  • <main>: 64px top bar (menu button for phone, search field), then .panes: the 520px .col (header + list) and the .read empty pane.

Tokens

:root {
  /* surfaces */
  --bg: #FAFAF7;          /* main and popover */
  --side: #F0EFEA;        /* sidebar, search field, row hover */
  --hover: #E5E4DD;       /* sidebar row hover, meter track */
  --sel: #F6E3EA;         /* current folder fill */
  --line: #DEDCD4;        /* hairlines */
  --line-2: #8C897F;      /* field and kbd borders, 3.3:1 on --side */

  /* text */
  --ink: #1D1C1A;         /* 15.6:1 on --side */
  --ink-2: #5A5852;       /* folder names, totals, 6.3:1 */
  --ink-3: #6E6B64;       /* section headings, 4.7:1 */

  /* accent */
  --accent: #9F1D4A;      /* Compose, current folder text, unread dot, meter segment 1 */
  --accent-ink: #FFF5F8;  /* text on accent, 8.4:1 */
  --accent-soft: #F6E3EA;

  /* label chips */
  --l1: #2F7D6D;          /* Clients */
  --l2: #C0782A;          /* Invoices */
  --l3: #3F6FB5;          /* Travel */
  --l4: #B04A8C;          /* Family */
  --l5: #7A7A70;          /* Newsletters */

  /* type */
  --font: "Work Sans", system-ui, sans-serif;

  /* layout */
  --w: 272px;             /* sidebar */
  --col: 520px;           /* hinted list */
  --row: 34px;            /* folder row */
  --r: 8px;
  --r-compose: 14px;

  /* depth */
  --shadow-pop: 0 12px 32px -8px rgba(29, 28, 26, .22), 0 0 0 1px rgba(29, 28, 26, .08);

  /* motion */
  --t-fast: 150ms;
  --t-mid: 280ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
Folder nameWork Sans14px40034px row0sentence
Folder, unreadWork Sans14px60034px row0sentence
Count, totalWork Sans12px40010tabular
Count, unreadWork Sans12px70010tabular
Compose labelWork Sans15px60010sentence
Section headingWork Sans11px6001+0.1emUPPERCASE
Account nameWork Sans14px6001.30Title
Storage, legendWork Sans11–12px400/6001.30sentence
List headerWork Sans22px6001.2−0.02emsentence
List senderWork Sans14px500 / 700 unread1.40Title

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
Section foldheading clickgrid-template-rows1fr → 0fr280ms--easeinert set on close
Section chevronheading clicktransform0 → rotate(−90deg)280ms--ease
Composehover / activebackground, scale→ #86163D, scale(.98)150ms--ease
Shortcuts card? / clickopacity, translateY0, 6px → 1, 0150 / 280ms--ease, --ease-out
Drawer (≤760px)menu clicktransformtranslateX(−100%) → 0280ms--ease-outshadow only while open
Scrimdraweropacity0 → 1280ms--easergba(29,28,26,.4)

Reduced motion: every transition is 1ms. Folding, the card and the drawer still work, instantly.

States

  • Folder hover: --hover fill, --ink text.
  • Current folder: aria-current="page", --sel fill, --accent text and count, weight 600.
  • Unread: folder name 600, count 700 --ink; in the list, sender and subject 700 plus a 7px raspberry dot at the row’s left.
  • Total-only count: 400 --ink-2. Never show a zero; drop the count instead.
  • Section folded: chevron −90°, body inert, height 0.
  • Empty folder: list shows a bold one-line title and a quiet second line, no illustration.
  • Focus-visible: 2px --accent outline at 2px offset; on Compose the outline is --ink so it shows against the raspberry fill.
  • Storage near full (> 90%): first segment and label switch to a warning; this demo sits at 76%, so the upsell is a quiet link, not a banner.

Accessibility

  • The aside is labelled “Mail folders”; folders sit in <nav aria-label="Folders">. Folder rows are buttons in this demo because they swap the list in place; in a routed app use links with aria-current="page".
  • Counts are inside the button text, so “Inbox 24” is read as one name. If you want “24 unread” read aloud, add a visually hidden ” unread” after unread counts only.
  • Section headers are buttons with aria-expanded and aria-controls; a folded body gets inert.
  • The storage bar is role="meter" with aria-valuenow="11.4", aria-valuemax="15" and a label that says it in words.
  • Shortcuts: C, G then I/S/D, /, ?, Esc. They never fire inside inputs or with Cmd, Ctrl or Alt held, so browser and screen reader keys are untouched. The ? hint is visible on screen, not only in a tooltip.
  • Drawer: the menu button has aria-expanded and aria-controls="side"; opening moves focus to the current folder; Esc or the scrim closes.
  • Contrast on --side: --ink 15.6:1, --ink-2 6.3:1, --ink-3 4.7:1; --accent on --sel 6.9:1; --accent-ink on --accent 8.4:1. The search field and kbd borders use --line-2 at 3.3:1.
  • Hit targets: folder rows 34px with 8px radius on a 272px row; Compose 48px; menu button 40px.

Responsive rules

  • ≥ 1280: sidebar 272px, list 520px, reading pane takes the rest.
  • 1024–1279: list narrows to 400px; sidebar unchanged.
  • 761–1023: same, the reading pane shrinks; the sidebar never collapses to a rail in this piece (mail folders need their counts).
  • ≤ 760: sidebar becomes a 272px off-canvas drawer with a scrim; the top bar gains the menu button; the list fills the width; the reading pane is hidden; the search / hint hides; the shortcuts card spans 12px from each edge; list subjects clamp to one line and snippets wrap to two (-webkit-line-clamp) instead of ellipsising past the edge. No sideways scroll at 390.

Acceptance checklist

Always

  • Unread counts are bold and bold their folder name; total counts are regular grey.
  • One filled primary in the sidebar: Compose, with its shortcut shown.
  • Exactly one folder has aria-current="page" and the list header matches it.
  • Label sections fold with aria-expanded and the folded body is inert.
  • Storage is a meter with a text label, a split bar and a legend.
  • Shortcuts are taught on screen (?) and never fire while typing.
  • Below 760px the sidebar is a drawer with a scrim, Esc to close and focus moved in.
  • Focus is visible on every control, including on the raspberry Compose button.

This demo

  • Sidebar 272px; folder rows 34px; Compose 48px tall, radius 14px, #9F1D4A.
  • Folders in order: Inbox 24, Starred, Snoozed 3, Sent, Drafts 2, Scheduled 1, Spam 12, Trash.
  • Labels Clients 6, Invoices 2, Travel, Family, Newsletters 31 with chips #2F7D6D, #C0782A, #3F6FB5, #B04A8C, #7A7A70.
  • Storage reads “11.4 GB of 15 GB” and 76%, split Files 6.8, Mail 3.1, Photos 1.5.
  • G then I, S or D within 900ms switches folder.

Implementation notes

Fold without measuring heights. Wrap each section body in a one-row grid and animate the row between 1fr and 0fr; the child needs overflow: hidden.

.fold { display: grid; grid-template-rows: 1fr;
        transition: grid-template-rows 280ms cubic-bezier(.2,.7,.2,1); }
.fold > ul { overflow: hidden; }
.fold.shut { grid-template-rows: 0fr; }
.sh button[aria-expanded="false"] .car { transform: rotate(-90deg); }

Two-key shortcuts with a short window. Remember when G was pressed and only accept the second key within 900ms:

let g = 0;
addEventListener('keydown', (e) => {
  if (/INPUT|TEXTAREA/.test(e.target.tagName) || e.metaKey || e.ctrlKey || e.altKey) return;
  const k = e.key.toLowerCase();
  if (g && Date.now() - g < 900) { g = 0; const to = { i: 'inbox', s: 'starred', d: 'drafts' }[k];
    if (to) { go(to); e.preventDefault(); } return; }
  if (k === 'g') g = Date.now(); else if (k === 'c') compose.click();
  else if (e.key === '?') toggleKeys(); else if (k === '/') { e.preventDefault(); search.focus(); }
});

Count grammar lives in a class, not in the number. Mark folders that hold unread mail with .unread; style weight from that, so a folder with “2” drafts never looks like it has two unread.

.f .n { margin-left: auto; font-size: 12px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.f.unread { color: var(--ink); font-weight: 600; }
.f.unread .n { font-weight: 700; color: var(--ink); }

The field edge belongs to the input. The visible field box (fill, 1px border, radius) is drawn on the <input> itself, absolutely filling its <label class="search">, with the icon and kbd layered above it (position: relative; z-index: 1; pointer-events: none). A border on the wrapper does not count as the control’s edge. Hide the closed drawer with visibility: hidden (transitioned with a delay equal to the slide, visibility 0s linear <dur>, and transition-delay: 0s when opening) so off-screen text is never rendered; add inert too if your stack supports it.

Common mistakes: putting the drawer’s box-shadow on the closed state (it leaks a grey edge onto the page at phone width); animating height from auto; letting G alone trigger navigation; colouring every count raspberry, which turns the sidebar into a wall of badges.

Details

Palette
Type
Work Sans
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

60fps

Show one small moment at a time, and let the tags do the sorting.

All sources