Skip to content
how-to-build-chatgpt-sidebar logo

How to Build a ChatGPT-Style Sidebar

how-to-build-chatgpt-sidebar

Use when asked to build a sidebar experience similar to ChatGPT.com / OpenAI

SKILL.md

Full skill instructions

How to Build a ChatGPT-Style Sidebar

This skill documents the reusable patterns, DOM structure, layout, animations, and information architecture reverse-engineered from ChatGPT.com's sidebar. Use this when building a collapsible sidebar that follows the same UX.

Reference screenshots and HTML snapshots live in references/ — consult references/​README.md for the full inventory.


Overall Page Layout

The page is a full-viewport flex column. The sidebar and main content sit side-by-side inside a flex row.

body
  └── div.flex.h-svh.w-screen.flex-col
        └── div.relative.z-0.flex.min-h-0.w-full.flex-1       ← flex row
              ├── #sidebar-container (shrink-0, border-e)      ← sidebar
              └── @container/​main (flex-1, min-w-0)             ← main content

Key layout rules:

  • The sidebar is shrink-0 so it never compresses; the main content area uses flex-1 min-w-0 to fill remaining space.
  • The sidebar has a right border (border-e for logical properties / border-r for LTR).
  • The sidebar is h-full and overflow-hidden.
  • The sidebar is z-21 (above main content, below modals/​tooltips at z-50).
  • The sidebar is hidden on mobile via max-md:hidden and print:hidden.

Sidebar States

State 1: Desktop Collapsed (Rail)

Container: width: var(--sidebar-rail-width) (~52px), background-color: var(--sidebar-bg, var(--bg-primary)).

The container holds two overlapping layers stacked via absolute positioning:

  1. Tiny bar (#stage-sidebar-tiny-bar): absolute inset-0, opacity-100, interactive.
  2. Expanded panel: opacity-0, pointer-events-none, inert — present in DOM but invisible/​inert.

The tiny bar is a vertical flex column:

div#stage-sidebar-tiny-bar.flex.h-full.w-[--sidebar-rail-width].flex-col.items-start
  ├── div.h-header-height.flex.items-center.justify-center    ← logo / toggle button
  ├── div.mt-[--sidebar-section-first-margin-top]             ← icon-only nav items
  │     ├── a (New Chat icon)
  │     ├── div (Search icon)
  │     └── a (Library icon)
  ├── div.pointer-events-none.flex-grow                        ← spacer pushes profile down
  └── div.mb-1                                                 ← profile avatar
        └── div.__menu-item[data-size="large"] (profile button)

Icon swap on hover: The toggle button contains two SVG icons. The logo icon has group-hover/​tiny-bar:hidden and the panel-toggle icon has hidden group-hover/​tiny-bar:block. When hovering anywhere on the tiny bar (group/​tiny-bar), the logo swaps to a panel icon. This is pure CSS, no JS.

State 2: Desktop Expanded

Container: width: var(--sidebar-width) (260px), background-color: var(--sidebar-bg, var(--bg-elevated-secondary)).

The same two layers exist, but visibility is swapped:

  1. Tiny bar: opacity-0, pointer-events-none, inert.
  2. Expanded panel: opacity-100, interactive.

The expanded panel is a scrollable full nav:

div.opacity-100.h-full.w-[--sidebar-width].overflow-y-auto
  ├── h2 (screen-reader only: "Chat history")
  └── nav[aria-label="Chat history"].flex.h-full.flex-col.overflow-y-auto
        ├── div.sticky.top-0.z-30 (sidebar header)
        │     └── div#sidebar-header.h-header-height.flex.items-center.justify-between
        │           ├── a[aria-label="Home"] (logo icon, links to /)
        │           └── div.flex
        │                 └── button[aria-label="Close sidebar"] (toggle)
        ├── aside (action items: New Chat, Search, Library — with text labels)
        ├── [scrollable content sections]
        └── [footer / profile at bottom of scroll]

State 3: Mobile Closed

  • No sidebar container in DOM — the #stage-slideover-sidebar element does not exist at mobile breakpoints.
  • The main content area fills the full width.
  • The header contains a hamburger button:
button[data-testid="open-sidebar-button"][aria-controls="stage-popover-sidebar"]
  ├── span.sr-only "Open sidebar"
  └── svg (hamburger icon)

State 4: Mobile Open (Dialog/​Popover)

The sidebar renders as a fixed dialog with a backdrop overlay:

div.fixed.inset-0.z-10.bg-gray-50/​50.dark:bg-black/​50         ← scrim/​backdrop
div#stage-popover-sidebar[role="dialog"].fixed.start-0.top-0.z-50.h-full.w-[--sidebar-width].max-w-xs
  ├── h2 (screen-reader only: "Sidebar")
  ├── h2 (screen-reader only: "Chat history")
  └── nav (same internal structure as desktop expanded panel)

Key mobile dialog styles:

  • border-e border-gray-200 dark:border-gray-800
  • bg-(--sidebar-moweb-bg, var(--sidebar-surface-primary))
  • shadow-[0_0_64px_0_rgba(0,0,0,0.07)]
  • pb-[env(safe-area-inset-bottom,0px)] for iOS safe area
  • The close button inside the sidebar aria-controls="stage-popover-sidebar"

CSS Custom Properties

:root {
  --sidebar-width: 260px;
  --sidebar-rail-width: calc(13 * var(--spacing)); /​* ~52px */
  --header-height: calc(13 * var(--spacing)); /​* ~52px */
  --sidebar-section-margin-top: 1.25rem;
  --sidebar-section-first-margin-top: 0.5rem;
  --sidebar-expanded-section-margin-bottom: 1.25rem;

  /​* Light mode */
  --sidebar-bg: #ffffff29;
  --sidebar-mask-bg: transparent;
  --sidebar-surface-primary: var(--gray-50);
  --sidebar-surface-secondary: var(--gray-100);
}

/​* Touch devices get larger targets */
@media (pointer: coarse) {
  :root {
    --sidebar-rail-width: calc(14 * var(--spacing)); /​* ~56px */
    --header-height: calc(14 * var(--spacing)); /​* ~56px */
  }
}

/​* Dark mode */
.dark {
  --sidebar-bg: #00000029;
  --sidebar-surface-primary: var(--gray-900);
  --sidebar-surface-secondary: var(--gray-800);
}

Open/​Close Animation

The sidebar does NOT animate width. Instead, it uses a cross-fade between two overlapping layers:

  1. The container width changes instantly via CSS variable (--sidebar-rail-width ↔ --sidebar-width).
  2. The tiny bar and expanded panel cross-fade using opacity transitions:
    • motion-safe:transition-opacity motion-safe:duration-150 motion-safe:ease-linear
  3. The tiny bar uses a stepped easing: motion-safe:ease-[steps(1,start)] — it snaps instantly rather than fading, preventing a ghostly half-state.
  4. The non-active layer gets pointer-events-none and the inert HTML attribute to prevent focus and interaction.

This approach avoids layout shift during animation and is significantly simpler than animating width.


Menu Item System (.__menu-item)

This is the core primitive for all sidebar items: nav actions, chat history entries, profile button.

Base styles

.__menu-item {
  min-height: var(--menu-item-height);
  padding-inline: calc(var(--spacing) * 2.5);
  padding-block: calc(var(--spacing) * 1.5);
  font-size: var(--text-sm);
  border-radius: 10px;
  display: flex;
  align-items: center;
  cursor: pointer;
  user-select: none;
  margin-inline: calc(var(--spacing) * 1.5); /​* gutter from edges */
}

.__menu-item[data-fill] {
  max-width: calc(100% - 3 * var(--spacing)); /​* constrained width */
}

.__menu-item[data-size="large"] {
  min-height: calc(var(--spacing) * 10); /​* profile button */
}

Hover/​active states

.__menu-item {
  --menu-item-highlighted: var(--interactive-bg-secondary-hover);
  --menu-item-active: var(--interactive-bg-secondary-press);
}

.__menu-item.hoverable:hover {
  background-color: var(--menu-item-highlighted);
}

.__menu-item:active:not(:has([data-trailing-button]:hover)) {
  background-color: var(--menu-item-active);
}

Trailing action pattern

Menu items use a grid overlay to show/​hide trailing action buttons on hover:

<a class="group __menu-item hoverable" data-fill>
  <div class="flex min-w-0 grow items-center gap-2.5">
    <div class="truncate"><span>Chat Title</​span></​div>
  </​div>
  <div class="trailing-pair">
    <div class="trailing highlight">
      <!-- Visible on hover: options button -->
      <button class="__menu-item-trailing-btn">...</​button>
    </​div>
    <div class="trailing">
      <!-- Default trailing content (hidden on hover) -->
    </​div>
  </​div>
</​a>

CSS mechanics:

  • .trailing-pair uses display: inline-grid with both children in the same grid cell (overlap).
  • .trailing.highlight starts at opacity: 0 (or clip-path: inset(50%) for [data-fill] items).
  • On hover/​focus/​open: .trailing.highlight becomes visible, .trailing:not(.highlight) hides.
  • The trailing button itself uses negative margins to extend clickable area to the menu item edge.

Sidebar Content Sections

Action Items (sticky section)

Pinned at the top of the scrollable area using a sticky <aside>:

aside.pt-[--sidebar-section-first-margin-top].tall:sticky.tall:top-header-height.tall:z-20
  ├── New chat (a.__menu-item with icon + label + keyboard shortcut)
  ├── Search chats (div.__menu-item with icon + label + keyboard shortcut)
  └── Library/​Images (a.__menu-item with icon + label)

Chat History

A flat container of conversation links — no <ol>/<li> structure:

div#history
  ├── a.__menu-item.hoverable[draggable][href="/​c/​..."]
  ├── a.__menu-item.hoverable[draggable][href="/​c/​..."]
  └── ...

Each conversation item has:

  • data-sidebar-item="true" for sidebar-specific behavior
  • draggable="true" for drag-and-drop reordering
  • A truncated title with dir="auto" for bidirectional text
  • A .trailing-pair with an options menu button (revealed on hover)

Profile (bottom)

Located in the tiny bar at the bottom via flex-grow spacer:

div.mb-1
  └── div.__menu-item[data-size="large"][data-testid="accounts-profile-button"]
        └── div.icon-lg
              └── div.rounded-full.h-6.w-6 > img (avatar)

In expanded mode, the profile shows next to the avatar: name + subscription tier.


Tooltip Pattern

Toggle buttons use Radix UI tooltips:

<span data-state="instant-open|delayed-open|closed">
  <button aria-label="Open sidebar" aria-describedby="radix-...">
    <!-- icon -->
  </​button>
</​span>

<!-- Portal-rendered tooltip -->
<div
  data-radix-popper-content-wrapper
  style="position: fixed; transform: translate(Xpx, Ypx); z-index: 50;"
>
  <div
    data-side="right|bottom"
    data-align="center"
    class="dark bg-black rounded-lg px-2 py-1 max-w-xs"
  >
    <div class="text-xs font-semibold text-center">Open sidebar</​div>
  </​div>
</​div>

Tooltip positioning:

  • Rail button tooltip: data-side="right" (appears to the right of the rail)
  • Expanded close button tooltip: data-side="bottom" (appears below the button)

Responsive Strategy

BreakpointSidebar BehaviorToggle Mechanism
md and aboveInline rail/​expanded panel (#stage-slideover-sidebar)Logo icon click (rail→expand), close button (expand→rail)
Below mdDialog/​popover (#stage-popover-sidebar) with backdropHamburger button in header opens, X/​scrim closes

Key responsive classes:

  • max-md:hidden — hides the inline sidebar on mobile
  • md:hidden — hides mobile-only elements on desktop
  • touch:h-10 touch:w-10 — larger touch targets (44px) on coarse pointer devices
  • touch:px-1.5 — adjusted padding for touch

Accessibility

  • <nav aria-label="Chat history"> wraps the sidebar content
  • <h2> (visually hidden) provides heading structure for screen readers
  • Toggle buttons use aria-expanded, aria-controls, and aria-label
  • Mobile sidebar uses role="dialog" with aria-labelledby/aria-describedby
  • sr-only / clip-rect pattern for screen-reader-only text
  • keyboard-focused:bg-token-surface-hover for keyboard-specific focus styles (separate from mouse hover)
  • RTL support: data-rtl-flip on directional icons, rtl:cursor-w-resize, :dir(ltr)/:dir(rtl) selectors
  • inert attribute on hidden layers to prevent screen reader traversal

Key Reusable Patterns (Summary)

  1. Rail + Expand via opacity cross-fade: Two absolutely-positioned layers inside a container whose width changes via CSS variable. No width animation — just opacity transition + inert.

  2. Icon swap on hover: Use Tailwind group-hover/​{name}:hidden / group-hover/​{name}:block on two sibling SVGs to swap icons (e.g., logo → panel toggle) without JS.

  3. Menu item primitive: A flex row with border-radius: 10px, hover highlight, and a trailing-pair grid for overlay action buttons that appear on hover.

  4. Mobile dialog + backdrop: At mobile breakpoints, render sidebar as a position: fixed dialog with role="dialog", a semi-transparent backdrop (bg-gray-50/​50), and safe-area inset padding.

  5. Sticky action section: Use <aside> with sticky top-[header-height] for pinned action items (New Chat, Search) above the scrollable chat history.

  6. Touch-adaptive sizing: Use touch: variant (pointer: coarse media query) to increase hit targets from 36px to 40px.

  7. pointer-events-none + inert: Always pair these on hidden layers to prevent both mouse and keyboard/​screen-reader interaction.

More skills from diegosouzapw

accessibility logo
diegosouzapw/awesome-omni-skill

accessibility

Accessibility guidelines for VS Code features — covers accessibility help dialogs, accessible views, verbosity settings, accessibility signals, ARIA alerts/status announcements, keyboard navigation, and ARIA labels/roles. Applies to both new interactive UI surfaces and updates to existing feature...

62 1
View
mermaid-diagrams logo
diegosouzapw/awesome-omni-skill

mermaid-diagrams

Mermaid diagram creation for flowcharts, sequences, ERDs, and more. Generate diagrams from text in markdown files. Use for documentation, architecture diagrams, and visual representations. Triggers on mermaid, flowchart, sequence diagram, ERD, entity relationship, gantt chart, pie chart, class di...

62 1
View
tauri2-react-rust logo
diegosouzapw/awesome-omni-skill

tauri2-react-rust

Guides development of cross-platform desktop apps with Tauri 2, TypeScript, React, and Rust. Use when building Tauri apps, implementing IPC, designing Rust backend or TypeScript/React frontend, when researching or cloning a website (open site, snapshot elements), when verifying local dev or built...

62 1
View
fp-ts-errors logo
diegosouzapw/awesome-omni-skill

fp-ts-errors

Handle errors as values using fp-ts Either and TaskEither for cleaner, more predictable TypeScript code. Use when implementing error handling patterns with fp-ts.

62 0
View
ring:pre-dev-research logo
diegosouzapw/awesome-omni-skill

ring:pre-dev-research

Gate 0 research phase for pre-dev workflow. Dispatches 3 parallel research agents to gather codebase patterns, external best practices, and framework documentation BEFORE creating PRD/TRD. Outputs research.md with file:line references.

62 0
View
create-assistant logo
diegosouzapw/awesome-omni-skill

create-assistant

Create and configure Vapi voice AI assistants with models, voices, transcribers, tools, hooks, and advanced settings. Use when building voice agents, phone bots, customer support assistants, or any conversational AI that handles phone or web calls.

62 0
View
codex-headless logo
diegosouzapw/awesome-omni-skill

codex-headless

Delegiere Aufgaben an OpenAI Codex CLI im Headless-Mode. Nutzt ChatGPT Subscription (KEIN API Key). Nur Workspace-Zugriff, KEIN System-Zugriff. Auto-Accept mit --full-auto. Web-Recherche mit --search.

62 0
View
social-media-scheduler logo
diegosouzapw/awesome-omni-skill

social-media-scheduler

Generate a full week of social media content for any topic. Outputs platform-optimized posts for Twitter/X, LinkedIn, and Instagram with hashtags and posting times.

62 0
View
Data Privacy Compliance logo
diegosouzapw/awesome-omni-skill

Data Privacy Compliance

Data privacy and regulatory compliance specialist for GDPR, CCPA, HIPAA, and international data protection laws. Use when implementing privacy controls, conducting data protection impact assessments, ensuring regulatory compliance, or managing data subject rights. Expert in consent management, da...

62 0
View
triple-review logo
diegosouzapw/awesome-omni-skill

triple-review

Reviews a task using the TRIPLE protocol, as the review agent.

62 0
View
Tech Stack & Architecture Decision logo
diegosouzapw/awesome-omni-skill

Tech Stack & Architecture Decision

Define and document the technology stack and architecture decisions for a project. Use when the user needs to choose a tech stack, make architecture decisions, define infrastructure choices, or document technology selections. Triggers on requests like "define the tech stack", "choose technologies...

62 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View