Source Code

<div class="ws">
  <nav class="ws-rail">
    <button type="button" class="ws-rail-btn active" data-panel="files" title="Files" aria-label="Files">▤</button>
    <button type="button" class="ws-rail-btn" data-panel="search" title="Search" aria-label="Search">⌕</button>
    <button type="button" class="ws-rail-btn" data-panel="git" title="Source Control" aria-label="Source Control">⑂</button>
    <button type="button" class="ws-rail-btn" data-panel="ext" title="Extensions" aria-label="Extensions">▣</button>
    <button type="button" class="ws-rail-btn ws-rail-bottom" data-panel="settings" title="Settings" aria-label="Settings">⚙</button>
  </nav>

  <aside class="ws-list" id="wsList" style="width:230px">
    <div class="ws-list-head" id="wsListHead">Files</div>
    <div class="ws-list-body" id="wsListBody"></div>
    <div class="ws-resizer" id="wsResizer"></div>
  </aside>

  <main class="ws-main">
    <div class="ws-tabs">
      <div class="ws-tab active"><span>layout.css</span><i>×</i></div>
      <div class="ws-tab"><span>index.html</span><i>×</i></div>
    </div>
    <div class="ws-editor" id="wsEditor">
      <pre><code>.ws {
  display: grid;
  grid-template-columns: 52px var(--list-w) 1fr;
  height: 100vh;
}

.ws-rail   { /* fixed icon rail   */ }
.ws-list   { /* resizable pane    */ }
.ws-main   { /* fluid detail pane */ }</code></pre>
    </div>
    <div class="ws-statusbar">
      <span id="wsActivePanel">Files</span>
      <span class="ws-dot">·</span>
      <span id="wsListWidth">230px</span>
      <span class="ws-spacer"></span>
      <span>Ln 5, Col 12</span>
    </div>
  </main>
</div>

IDE Three-Pane Workspace Layout — Icon Rail + Resizable List

IDE Three-Pane Workspace Layout · Layouts · Plain HTML, CSS & JS · Live preview

What's included

Features

Fixed-width icon rail, resizable list pane, and fluid main pane in one CSS grid declaration
Rail icons act as real view switchers — list pane heading and content change identity per panel, not just visibility
Pointer-capture drag resize on the list pane, matching the robust drag pattern used for resizable table columns
Resize is clamped between a minimum and maximum width so the pane can never be dragged unreadable or too wide
Live pixel-width readout mirrored to the status bar during a resize drag
Realistic main pane content — tabs with close affordances, a code preview, and a bottom status bar
Five pre-built panel datasets (files, search, source control, extensions, settings) driving the same list markup
Responsive: the list pane becomes an overlay drawer next to the rail on narrow viewports instead of squeezing the editor

About this UI Snippet

IDE Three-Pane Workspace Layout — Fixed Icon Rail, Resizable List Pane & Fluid Editor

Screenshot of the IDE Three-Pane Workspace Layout snippet rendered live

Tools like VS Code, Slack, and most modern IDEs share the same three-zone skeleton: a narrow fixed-width icon rail on the far left for switching top-level views, a resizable list pane next to it showing the content of whichever view is active, and a large fluid pane on the right doing the actual work. This snippet builds that exact shell — icon rail, resizable file/search/git list, and an editor pane with tabs and a status bar — entirely in HTML, CSS grid, and vanilla JavaScript.

Three zones, one grid declaration

The outer .ws container is a single grid-template-columns: 52px auto 1fr row filling 100vh: a fixed-width rail, an auto-sized list pane whose width is actually controlled by inline JavaScript, and a fluid 1fr main pane that absorbs whatever space is left. Because the rail's width never changes, it behaves as a stable anchor for the eye no matter how the middle pane is resized or which panel is active — exactly the property that makes an icon rail useful as permanent muscle-memory navigation.

The rail switches content, not just an active state

Clicking a rail button does two things: toggles the .active class for the pressed-icon visual state, and calls renderPanel(key), which looks up a PANELS object keyed by 'files', 'search', 'git', 'ext', or 'settings' and re-renders the list pane's heading and item rows from that panel's own data. This models the real behavior of a rail-driven workspace: the rail is not a static sidebar toggle, it is a *view switcher* — the list pane's entire content and heading change identity depending on which rail icon is active, not just its visibility.

Resizing the middle pane with pointer capture

The drag handle on the list pane's right edge uses the same pointerdown / pointermove / pointerup plus setPointerCapture() pattern used for any robust drag interaction — capturing the pointer means the resize keeps tracking correctly even if the cursor moves faster than the browser can repaint and briefly leaves the 8px hit area. The width is clamped between 160px and 480px so the pane can never be dragged small enough to make its own contents unreadable, or wide enough to swallow the entire editor pane, and a status-bar readout mirrors the live pixel width back to the user during the drag.

A realistic detail pane, not a placeholder

Rather than an empty gray box, the main pane includes tabs (with a close affordance), a monospace code preview, and a status bar — the same three sub-elements every code editor's main area has. This matters for a layout snippet specifically because a layout's true test is whether *realistic* content fits it correctly: tabs need to sit above a scrollable body, and a status bar needs to stick to the bottom of the pane regardless of how much code is above it, both of which this flex-column main pane handles.

Where this shell belongs

This is the right starting skeleton anywhere a product needs several distinct top-level "modes" (files, search, notifications, settings) that each drive a secondary list, next to one large primary work surface — internal admin tools, no-code builders, chat apps with channel/DM switching, and of course browser-based code editors.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the PANELS object drives both the rail's active state and the list pane's rendered content from the same click handler, and why the resize logic clamps width rather than letting the pane grow or shrink without limit. It is also a good candidate for extension — ask it to persist the chosen list-pane width and last-active rail panel to localStorage so they survive a page reload, add a collapse-to-icon-only mode for the list pane itself (distinct from the fixed rail), or wire real file-tree expand/collapse behavior into the Files panel instead of the flat mock list.

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 code-editor-style three-pane workspace layout in plain HTML, CSS Grid, and JavaScript — no framework, no library.

Requirements:
- A single outer CSS grid container with three columns: a fixed-width icon rail (roughly 52px), a resizable list pane, and a fluid main pane that fills all remaining space, together filling the full viewport height.
- The icon rail must contain at least five icon buttons (e.g. files, search, source control, extensions, settings), each toggling an active visual state and, more importantly, causing the list pane to re-render its heading and item rows with content specific to that icon — model this as a lookup object keyed by a panel identifier, each holding a label and an array of list items, so the list pane's identity actually changes per rail selection rather than just being shown or hidden.
- A drag handle on the right edge of the list pane that resizes its width using pointer events (pointerdown/pointermove/pointerup) with setPointerCapture() so the drag continues correctly even if the pointer leaves the narrow handle mid-gesture.
- Clamp the resize between a sensible minimum (so list text never becomes unreadable) and maximum (so the list pane can never swallow the main pane) width, and show the current pixel width live somewhere in the UI while dragging.
- The main pane must contain realistic content demonstrating the layout actually works: a row of tabs with a close affordance, a scrollable code or content preview area, and a status bar pinned to the bottom of that pane showing the currently active rail panel's label.
- Make the layout responsive: on a narrow viewport, the list pane should become an overlay/drawer positioned next to the icon rail rather than permanently squeezing the main pane's width.

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
    Click a rail iconSwitching between Files, Search, Source Control, Extensions, or Settings re-renders the list pane's heading and items for that view.
  2. 2
    Drag the divider to resize the list paneGrab the edge between the list pane and the editor and drag — the width is clamped between 160px and 480px and mirrored live in the status bar.
  3. 3
    Replace the PANELS dataEdit the PANELS object in the JS panel to swap in your own file tree, search results, or any other list content per rail icon.
  4. 4
    Add or remove rail iconsAdd a new button with a data-panel attribute matching a new key in PANELS, and it wires into the same click handler automatically.
  5. 5
    Fill in the editor paneReplace the tabs and code preview in .ws-main with your actual editor, preview, or detail content — the pane is a plain flex column.
  6. 6
    Export in your frameworkUse the HTML, JSX, Vue, or Tailwind export buttons to bring this shell into your own project as a layout component.

Real-world uses

Common Use Cases

Browser-based code editors and IDEs
The exact skeleton behind VS Code, StackBlitz, and CodeSandbox — file tree, search, and source control all sharing one resizable list pane next to the editor.
Internal admin and ops tools
Rail-driven navigation between distinct top-level modes (users, logs, deployments) each with their own list, next to a detail pane — pairs well with the Dashboard Layout.
CHAT
Chat and messaging apps
Rail for switching between DMs, channels, and threads, each rendering its own conversation list, next to the active conversation pane.
No-code and visual builder tools
Switch between a component library, layers panel, and asset manager in the same resizable list slot beside the canvas.
Related: Resizable Split Pane
See the Resizable Split Pane for the same pointer-capture drag technique applied to a simpler two-pane layout.

Got questions?

Frequently Asked Questions

The rail functions as permanent muscle-memory navigation — its icons should always land under the same pixel position regardless of what else on the page is resized, the same way a taskbar or dock stays fixed. Making the rail itself resizable would undermine that stability for very little benefit, since its content (a handful of icons) never needs more room.

Each rail button carries a data-panel attribute matching a key in a PANELS JavaScript object holding a label and item list per view. The click handler looks up that key and calls renderPanel(), which rewrites the list pane's heading and rebuilds its item rows from that panel's own data — so the list is not just shown or hidden, its actual content changes identity.

Pointer events cover mouse, touch, and stylus input with one code path, and setPointerCapture() redirects all subsequent pointer events to the resizer element until release — without it, a fast drag can outrun the 8px hit area and the resize stops mid-gesture the moment the cursor leaves the handle.

Below roughly 160px, file or list names become unreadable and start truncating aggressively; above roughly 480px, the list pane starts crowding out the primary editor pane it is supposed to be secondary to. The clamp keeps the pane useful at both extremes without needing the user to manually recover from an unreasonable drag.

Read list.style.width (or the numeric value tracked during the drag) on pointerup and write it to localStorage; on page load, read it back before first paint and apply it to the pane's inline width, the same pattern used in the Resizable Sidebar with Persisted Width snippet.

Yes. Keep the PANELS data as component state or props, render the rail buttons and list items from it, and track the pane width in a ref during the drag (committing to state only on pointerup) so dragging does not re-render the whole tree on every pointermove event.