Design Lounge
Install

Nº 007 of 520

Collapsing sidebar rail

A 240px app sidebar that collapses to a 64px icon rail with hover tooltips, an accent active-route bar and a ⌘B shortcut.

Preview

Open full size
1280 × 800 · 10.2 KB · 116 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Collapsing sidebar rail" (piece collapsing-sidebar-rail) 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

184 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

A left-hand navigation sidebar for a dashboard-style web app. It has two widths: open (240px, icon + label + count badges) and rail (64px, icons only, labels become tooltips). A small round toggle button sits on the sidebar’s outer edge; ⌘B / Ctrl+B also toggles. The width animates, labels fade and slide out slightly ahead of the width change so nothing wraps mid-transition. The active route is marked by a 2px amber bar hugging the left edge, not by a filled pill. The feeling is quiet, dense, engineered: a tool you live in for hours.

Reference behaviour

  1. Initial state: sidebar is open at 240px. First nav item (“Dashboard”) is the current route. Main content shows a greeting, three stat cards and a bar chart.
  2. Hover any nav link: background changes to --panel-2, text becomes full --ink. No movement.
  3. Click the round toggle on the sidebar’s right edge (24px, straddling the border at right:-12px; top:66px): the sidebar shrinks to 64px over 320ms; labels, group headings and count badges disappear (opacity 0 + 6px leftward slide over 160ms, starting immediately); the chevron inside the toggle rotates 180°. Main content expands to fill the freed space.
  4. In rail state, hovering or keyboard-focusing a nav link shows a tooltip to the right of the rail: dark pill (--ink background, --bg text, 12px/500), 10px from the rail edge, with a 4px caret. It fades in and slides 4px over 160ms.
  5. Pressing ⌘B (macOS) or Ctrl+B (elsewhere) toggles the state exactly like the button.
  6. Click the toggle again: sidebar returns to 240px; labels fade in after the width starts growing.
  7. The toggle’s aria-expanded mirrors the state; its title reads “Collapse sidebar (⌘B)” or “Expand sidebar (⌘B)”.
  8. Hovering a bar in the chart tints it amber and shows its value in a native tooltip.

Structure

1280 × 800
┌──────────┬────────────────────────────────────────────────────────────┐
│ brand 56 │ topbar 56 ─ breadcrumb · · · · · · · · · · · · · ⌘B hint │
│──────────┼────────────────────────────────────────────────────────────┤
│ OVERVIEW │  h1 greeting                                               │
│ ▌Dashbrd │  subtitle                                                  │
│  Analytc │ ┌──────────┐ ┌──────────┐ ┌──────────┐                     │
│  Deploy12│ │ stat     │ │ stat     │ │ stat     │                     │
│ MANAGE   │ └──────────┘ └──────────┘ └──────────┘                     │
│  Members │ ┌────────────────────────────────────────┐                 │
│  Billing │ │ bar chart (48 bars)                    │                 │
│  Settngs │ └────────────────────────────────────────┘                 │
│          │                                                            │
│ user row │                                                            │
└──────────┴────────────────────────────────────────────────────────────┘
  240 → 64   (toggle sits at x = sidebar width − 12, y = 66)
  • <aside class="side" aria-label="Primary"> — flex column, width transitions.
    • .brand 56px tall: 28px rounded-square mark with a single glyph, product name (14/600), sub-line in mono 11px.
    • <button class="toggle" aria-expanded aria-controls="side"> absolutely positioned on the aside.
    • One .group per section: <h6> mono uppercase heading + <ul class="nav"> of <a> links. Each link: 18px SVG icon, .label span, optional .count badge, .tip tooltip span.
    • .spacer (flex 1) then .foot with the user row.
  • <main> — flex 1, min-width:0. .top 56px bar with breadcrumb and shortcut hint. .content is a 3-column grid of .cards; the chart card spans all three.

Tokens

:root {
  /* colour — warm near-black surfaces, one amber accent */
  --bg: #0f0f0e;          /* page */
  --panel: #161615;       /* sidebar + cards */
  --panel-2: #1d1d1b;     /* hover / active surface */
  --line: #2a2a27;        /* hairlines */
  --line-strong: #3a3a36; /* toggle border */
  --ink: #f2f0ea;         /* primary text */
  --ink-2: #a8a69e;       /* secondary text */
  --ink-3: #6f6e68;       /* tertiary / mono meta */
  --accent: #e0a34b;      /* active bar, toggle hover, bar hover */
  --accent-ink: #1a1400;  /* text on accent */
  --positive: #8fbf7a;    /* deltas */

  /* type */
  --font: "Inter", system-ui, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, monospace;

  /* layout */
  --w-open: 240px;
  --w-rail: 64px;
  --bar-h: 56px;          /* brand + topbar height */
  --r: 8px;               /* nav item radius */
  --r-card: 12px;

  /* motion */
  --t-fast: 160ms;
  --t-layout: 320ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
Body / navInter14px4001.450sentence
Brand nameInter14px6001.2−0.01emsentence
Group headingJetBrains Mono10px5001+0.12emUPPERCASE
Count badgeJetBrains Mono11px5001.40numerals
TooltipInter12px5001.30sentence
Page titleInter22px6001.2−0.02emsentence
Stat valueInter28px6001.1−0.03emnumerals
Card labelJetBrains Mono12px5001.3+0.06emUPPERCASE

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
.sidetogglewidth240px ↔ 64px320ms--easemain reflows via flex
.label, headingstoggleopacity, translateX1,0 → 0,−6px160ms--easestarts at t=0, so labels are gone before width crosses 160px
.toggle svgtogglerotate0 → 180°320ms--easesame clock as width
.tiphover/focus in railopacity, translateX0,−4px → 1,0160ms--easeonly when .rail present
nav link backgroundhoverbackground-colortransparent → --panel-20—instant; no transition on colour
.barhoverbackground--panel-2 → --accent160ms--ease

Reduced motion: set all transition-duration to 1ms. State changes still happen, instantly.

States

  • Hover (nav link): background --panel-2, colour --ink.
  • Focus-visible (nav link): two-ring box-shadow 0 0 0 2px var(--bg), 0 0 0 4px var(--accent). No outline.
  • Current route: aria-current="page", colour --ink, background --panel-2, plus a 2px × (height − 16px) amber bar at left:-10px (i.e., on the group’s padding edge).
  • Toggle hover: fill --accent, icon --accent-ink, border --accent.
  • Toggle focus-visible: 2px amber outline, 2px offset.
  • Rail state: .count badges are display:none; group headings opacity 0 but keep their 10px height so items don’t jump.

Accessibility

  • <aside aria-label="Primary"> wraps navigation; use <nav> inside if the framework prefers.
  • Toggle is a real <button> with aria-expanded and aria-controls pointing at the aside’s id.
  • Tooltips are visually-only duplicates of the label; the .label span stays in the DOM (opacity 0), so screen readers still read the link name. Do not display:none the label.
  • Keyboard: Tab moves through toggle → links → user row. ⌘B / Ctrl+B toggles; preventDefault so the browser’s bookmark shortcut doesn’t fire.
  • Contrast: --ink-2 on --panel is 7.9:1; --ink-3 is used only for meta text ≥ 11px mono (4.6:1).
  • Hit targets: nav rows 36px tall, toggle 24px visual but give it a 40px invisible hit area with ::after if the design system requires it.

Responsive rules

  • ≥ 1280: as specified.
  • 1024–1279: default to the rail state on load; the stat grid stays 3 columns.
  • 768–1023: rail state; stat grid 2 columns; chart spans both.
  • < 768: sidebar becomes an off-canvas drawer opened by a menu button in the topbar (240px, slides in over 240ms with a rgba(0,0,0,.5) scrim). The rail state does not exist below 768.

Acceptance checklist

  • Open width is exactly 240px and rail width exactly 64px.
  • Width animates over 320ms with cubic-bezier(.2,.7,.2,1); labels fade over 160ms starting at the same instant.
  • No label text wraps or overflows at any moment during the transition (use white-space: nowrap + overflow: hidden).
  • The toggle straddles the sidebar border (right: -12px) and rotates its chevron 180° in rail state.
  • ⌘B on macOS / Ctrl+B elsewhere toggles the state and does not open the bookmarks bar.
  • aria-expanded on the toggle flips with the state.
  • In rail state, hovering and keyboard-focusing a link shows the tooltip to the right.
  • The active route has a 2px amber bar on the left, not a filled pill.
  • Focus rings are visible on every link and on the toggle.
  • With prefers-reduced-motion: reduce, toggling is instantaneous but complete.
  • Main content reflows to use the freed space (no fixed left margin).
  • Body text contrast ≥ 4.5:1 on all surfaces.

Implementation notes

Labels must leave before the width crosses them. Transition the label on a shorter clock than the width and start both at once:

.side { width: var(--w-open); transition: width var(--t-layout) var(--ease); }
.side.rail { width: var(--w-rail); }
.label { transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); }
.rail .label { opacity: 0; transform: translateX(-6px); pointer-events: none; }

Tooltips only in rail state, positioned off the link, not the aside (the aside has overflow visible, so nothing clips):

.tip { position: absolute; left: calc(100% + 10px); top: 50%;
       transform: translate(-4px, -50%); opacity: 0; pointer-events: none; }
.rail .nav a:hover .tip, .rail .nav a:focus-visible .tip { opacity: 1; transform: translate(0, -50%); }

Keyboard shortcut — guard both modifiers and stop the browser default:

addEventListener('keydown', (e) => {
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'b') { e.preventDefault(); toggle.click(); }
});

Common mistakes: animating margin-left on main instead of letting flex reflow; hiding labels with display:none (breaks screen readers and kills the fade); forgetting min-width:0 on <main> so the chart overflows.

Details

Palette
Type
Inter · JetBrains Mono
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Tailwind CSS docs

Put each instruction next to its code. Draw the grid with thin lines and let the empty sides carry a faint hatch.

All sources