
Modern SaaS Dashboard Sidebar Menu with Native Dark Mode Switch
Published
30 hand-coded CSS sidebar layouts for SaaS admin dashboards, e-commerce filter pages, documentation sites, and webmail two-pane apps: SaaS dashboard shell with :has()-driven dark mode (Stripe / Linear / Vercel / Notion pattern), admin panel with [data-tooltip] icon collapse (Vercel Dashboard / Cursor Editor / Railway), e-commerce filter accordion with native <details> + interpolate-size (Shopify Dawn / Amazon / Etsy), documentation sidebar with scroll-synced nesting (Docusaurus / Mintlify / Nextra), two-pane webmail split (Gmail / Superhuman / Notion Mail), dev-tools inspector panel (VS Code / Chrome DevTools / Figma), off-canvas drawer via Popover API + @starting-style, subgrid analytics rail, container-query adaptive rail, and 21 more. Real <aside> / <nav> / <main> landmarks, aria-current / aria-expanded, 44px hit targets, :focus-visible everywhere. Scoped under .sbl-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Related10 CSS Sidebar Navigation Designs17 CSS Side Menu Designs30 CSS Grid Layouts

Published
Published

Published

<nav>, two personalities: below 768px it's an iOS-style bottom tab bar with safe-area padding; on desktop the very same markup stands up into a full left sidebar with labels beside icons. No duplicate nav trees to keep in sync, no JS resize listeners — a grid-area swap and a flex-direction flip do all of it.Published

Published
Published

Published

PublishedUpdated

PublishedUpdated

<dialog>-grade Popover. popover="auto" gives you Esc-to-close, click-outside dismissal, focus management and top-layer rendering — the whole 'drawer behavior' checklist browsers now ship natively — and @starting-style animates the entry, so even the open transition is declarative.PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated
PublishedUpdated

<details>/<summary> — every accordion section is keyboard-operable, screen-reader-announced, and crawlable with ZERO script. The panels animate open with the brand-new interpolate-size / height:auto transition, the chevrons rotate on [open], and the whole rail is position:sticky so filters ride along the product grid.PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

<dialog>. showModal()-free — this variant opens purely via the checkbox pattern for a zero-JS page — but keeps the dialog's visual grammar: content dims AND blurs behind a translucent scrim, the drawer lifts on a soft shadow, and pointer-events discipline means the dimmed page is genuinely inert to clicks.PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

<details> disclosure with rotating chevrons, indent guides drawn as borders so hierarchy stays legible at any depth, folder/file iconography, and progressive padding from a single --indent variable — the whole tree keyboard-operable out of the box because disclosure is native.PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated
Tweak the exact look in our visual generators — no signup, instant copy-paste.
overflow-y:auto on main gives you two independent scroll contexts, which is what Notion, Linear, and Figma all ship. When users need to reclaim horizontal real estate on 13-inch laptops, layer Demo 08 (Collapsible Drawer without JavaScript) on top, which uses a hidden checkbox plus :has(:checked) to flip state without a single line of JS. For mobile-primary apps where the sidebar is a secondary destination (settings, filters, account), Demo 10 (Off-Canvas Navigation Drawer) is the correct pattern — it uses the native popover attribute plus @starting-style for entry animation, so it degrades cleanly on older browsers as a static panel. Tablet-hybrid apps (768-1024px) benefit from Demo 18 (Push Content Sidebar), which animates grid-template-columns: 0fr 1fr to 1fr 4fr so the main content slides rather than being covered — this preserves reading position on wide-but-not-huge screens. For modal-style flows where you want focus trapped and the underlying UI dimmed, Demo 19 (Off-Canvas Overlay with Blur Backdrop) combines backdrop-filter:blur(12px) with a semi-opaque scrim. On viewport strategy, prefer @container queries over @media when the sidebar sits inside a parent whose width changes independently of the viewport (embedded dashboards, iframe demos, split views) — Demo 29 shows the pattern. Otherwise, use a breakpoint hybrid: @media (min-width:768px) for the tablet-to-desktop switch and container queries inside the sidebar for icon-only versus label mode. Avoid picking off-canvas as your only pattern on desktop; users hate hunting for hidden nav on a 27-inch monitor. And never build a push-content pattern on mobile — a 320px viewport pushed by a 280px drawer leaves 40px of readable content, which reads as broken. Match the pattern to the primary viewport, then layer a secondary pattern for the secondary viewport.:has(). Ship a visually hidden <input type="checkbox" id="sbl-toggle"> at the top of the shell, then style the sidebar with .sbl-shell:has(#sbl-toggle:checked) aside { transform: translateX(-100%); }. The <label for="sbl-toggle"> button becomes your toggle — it flips checkbox state on click and space/enter, so keyboard navigation works for free. Wire aria-expanded via a sibling CSS selector so screen readers hear the state change. This gives you a collapsible drawer with zero JS and full a11y. Demo 06 (Smooth Animated Collapsible with Icon-Only Mode) solves the harder problem: animating the sidebar from 240px labeled state to a 72px icon-only rail without jank. The trick is grid-template-columns: 240px 1fr transitioning to 72px 1fr, combined with a nested grid inside each nav item: grid-template-columns: 24px 1fr (icon plus label) transitioning to 24px 0fr. The 0fr unit is the modern breakthrough — fr units are now animatable in Chromium, Safari 16+, and Firefox 121+. Animating width: 0 instead breaks because it triggers layout thrash and the label text wraps mid-animation into an ugly two-line collapse before disappearing. With 0fr, the grid track itself collapses smoothly, and the label's overflow:hidden parent clips it cleanly. Pair with transition: grid-template-columns 240ms cubic-bezier(.4,0,.2,1) on both the shell and the item. Guard everything behind @media (prefers-reduced-motion: reduce) { * { transition-duration: 0.01ms !important; } }. For tooltips on the collapsed state, use [data-tooltip]::after { content: attr(data-tooltip); } gated by .sbl-shell.is-collapsed [data-tooltip]:is(:hover, :focus-visible)::after. That is the entire recipe — no JS, WCAG-compliant, ships in production.:has() relational selector, and a native <input type="checkbox">. Every color in the sidebar is a token: --bg-canvas, --bg-surface, --ink-primary, --ink-muted, --accent, --border-subtle. Define the light palette on :root, then override every token inside :root:has(#theme-toggle:checked) { --bg-canvas: #0b0d10; --ink-primary: #f5f7fa; }. Because every surface, border, and text color reads from a token, the entire UI flips theme instantly with zero repaint jank. This is exactly what Stripe Dashboard, Linear, Vercel Dashboard, Notion, and Superhuman ship — token-driven theming beats class-swap theming because it plays nicely with SSR (no flash of wrong theme on hydration) and third-party embeds. The sun/moon toggle itself is a hidden checkbox with a <label> that contains inline SVG icons for both states; use label:has(#theme-toggle:not(:checked)) .icon-moon { display:none } and vice versa to swap them. Critically, add :root { color-scheme: light dark; } to the base and :root:has(#theme-toggle:checked) { color-scheme: dark; } to the override — this tells the browser to render native scrollbars, form controls, and the caret in the correct theme, which is what makes the UI feel truly dark rather than a dark skin over a light shell. Pair this with a <meta name="color-scheme" content="light dark"> hint in the head so the initial paint respects OS preference before your CSS loads. For users who prefer OS-driven theming, wrap the override in @media (prefers-color-scheme: dark) { :root:not(:has(#theme-toggle:checked)) { /* dark tokens */ } } so the toggle acts as an explicit override on top of OS default. That is the entire Stripe/Linear/Vercel pattern in ~80 lines of CSS.<details><summary> for each filter group (Size, Color, Brand, Price) — no JS accordion library, no ARIA gymnastics, because <details> ships with role="group" and aria-expanded baked in. The modern upgrade is interpolate-size: allow-keywords on the root, which unlocks smooth height animation from 0 to auto — something CSS could not do before 2024. Pair with details::details-content { transition: block-size 300ms; block-size: 0; overflow: hidden; } and details[open]::details-content { block-size: auto; }. For exclusive-accordion behavior (opening one group closes the others), add name="filters" to every <details> in the sidebar — this is the 2024 HTML spec feature that Shopify Dawn, Shopify Debut, Amazon fashion filters, Etsy category refinement, and WooCommerce Storefront all now use. The filter rail itself is position: sticky; top: 1rem; max-height: calc(100dvh - 2rem); overflow-y: auto; so it follows the product grid as the user scrolls through hundreds of products without losing the filter controls. Use accent-color: var(--brand) on all <input type="checkbox"> and <input type="radio"> elements — this is the one-line native way to brand form controls without shipping a checkbox library. For price ranges, use <input type="range"> with accent-color, or a dual-thumb slider via two overlapping ranges. Filter chips (active filters shown above the grid) use display:flex; flex-wrap:wrap; gap:0.5rem with each chip being a <button> containing the filter value and an X icon. Semantic structure matters for SEO here: wrap the sidebar in <aside aria-label="Product filters"> so Google's crawlers, VoiceOver, and JAWS all announce it correctly. On mobile, flip the sidebar into a bottom sheet using the <dialog> element with showModal() — Amazon and Shopify both do this. That is the complete filter sidebar, framework-free.<details> at each level with the open attribute on the currently-active branch. The indent is a single padding-inline-start: 1rem on each nested <ul>, which cascades naturally. Demo 03 (Scroll-Synced Nesting) is the scroll-linked active state — use :target to highlight the anchor matching window.location.hash, and layer scroll-driven animations via animation-timeline: view() for the scroll-linked indicator that follows the reader down the page. This is what Docusaurus, Mintlify, Nextra, Astro Starlight, Docsify, and MkDocs Material all ship. The wrapper is position: sticky; top: 0; height: 100dvh; overflow-y: auto; overscroll-behavior: contain — the overscroll-behavior is critical because it prevents the parent page from bouncing when the user hits the sidebar's scroll boundary, which is the janky behavior that makes cheap docs sites feel broken. For the active-page indicator, use aria-current="page" on the anchor and style it via a[aria-current="page"] { background: var(--bg-active); color: var(--ink-primary); } — this is both semantic and screen-reader-announced. For search integration, place a sticky search input at the top of the sidebar (Demo 21 pattern) with position: sticky; top: 0; background: var(--bg-canvas). The <details> tree preserves open state across route changes if you name them consistently — no state library needed. Docs sidebars are one of the highest-CPM verticals on the web because dev-tools brands (Cloudflare, Vercel, Stripe, MongoDB, Twilio) all bid on position:sticky and docs-sidebar queries.display: grid; grid-template-columns: 360px 1fr; height: 100dvh — the message list gets a fixed width, the reading pane takes the rest. Inside the list pane, wrap the messages in <ul role="list"> so screen readers announce count and position, then make the list header (sender filter, sort, search) sticky via position: sticky; top: 0; z-index: 1; background: var(--bg-canvas). Each message row is a <button> or <a> with display: grid; grid-template-columns: 40px 1fr auto; gap: 0.75rem; align-items: start for avatar, sender+subject+preview, and timestamp. Focus management is where this pattern earns its price: every row needs :focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }, and the currently-selected message needs aria-current="true" plus a persistent visual state that survives blur. Keyboard nav is native — <button> elements get tab focus for free, and you can add tabindex="0" plus a keydown listener for j/k navigation (Gmail's shortcut). This is what Gmail, Superhuman ($30/month tier-1 SaaS), Notion Mail, HEY, Missive, and Front all ship. The pattern generalizes: swap the message list for a channel list and you have Slack DMs; swap it for a chat list and you have Discord; swap it for a file tree and you have Cursor Editor or VS Code. For unread indicators, use a <span aria-label="unread"> with a small dot styled via ::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }. For the reading pane's independent scroll, wrap its content in a div with overflow-y: auto; height: 100% — this is critical, because if the reading pane scrolls the whole page, the list header sticky breaks. Two-pane productivity SaaS is a $8-25 CPM vertical because the audience (knowledge workers, ops, sales) has enterprise budgets attached.grid-template-columns: 1fr 320px instead of 320px 1fr, with the inspector on the trailing edge. Use logical properties — border-inline-start: 1px solid var(--border-subtle) instead of border-left — so the layout mirrors correctly in RTL locales, which matters for enterprise dev-tools shipping to Middle East and Israeli teams. Combine with Demo 27 (Pure CSS Resizable Sidebar Using Resize Property) for the user-adjustable width that every dev-tool ships. The one-liner is aside { resize: horizontal; overflow: auto; min-inline-size: 240px; max-inline-size: 480px; direction: rtl; } plus aside > * { direction: ltr; } — the direction hack moves the native resize grabber from bottom-right to bottom-left, which is where users expect it on a right-aligned panel. This is a single CSS property replacing a 500-line JS drag handle library. For sticky sub-panels inside the inspector (Styles, Computed, Layout, Event Listeners in DevTools), use position: sticky; top: 0 on each panel header — Chrome DevTools does exactly this. Focus management is where dev-tools panels earn their trust: :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } on every interactive element, plus a keyboard-accessible tab strip if you have multiple inspector tabs (Demo 25's radio-input state machine handles this without JS). For the tree view inside the inspector (DOM tree, folder tree), Demo 24's nested <details> pattern renders correctly with just summary::marker { color: var(--ink-muted); } for the disclosure triangle. Dev-tools SaaS ($8-15 CPM tier because the audience is engineers at Series B+ companies with GitHub Copilot/Cursor/Linear/Vercel budgets) is a vertical worth optimizing for. Panels ship with <aside aria-label="Inspector"> and per-section <section aria-labelledby="styles-heading"> for correct screen-reader announcement.grid-template-columns: 240px 1fr collapsing to 64px 1fr, with a transition: grid-template-columns 240ms cubic-bezier(.4,0,.2,1). Inside each nav item, a nested grid grid-template-columns: 24px 1fr collapses to 24px 0fr, which cleanly clips the label without the width-animation jank. The tooltip is the key differentiator: when collapsed, users need to see the label to navigate. Use [data-tooltip]::after { content: attr(data-tooltip); position: absolute; inset-inline-start: calc(100% + 8px); background: var(--bg-tooltip); padding: 0.375rem 0.625rem; border-radius: 6px; opacity: 0; pointer-events: none; transition: opacity 120ms; }, then reveal via .sbl-shell.is-collapsed [data-tooltip]:is(:hover, :focus-visible)::after { opacity: 1; }. Critically, gate on both :hover AND :focus-visible — keyboard users need tooltips too, and this is where 80% of admin sidebars fail WCAG. Every hit target must be minimum 44px per WCAG 2.5.5 Target Size (AA): nav a { min-block-size: 44px; display: grid; align-items: center; }. Add aria-current="page" on the active link, aria-expanded plus aria-controls on the collapse toggle, and <aside aria-label="Main navigation"> on the wrapper. The collapse toggle itself should be a real <button type="button"> (not a div-with-onclick) so it works with Tab, Enter, Space, and screen readers. Store collapse state in localStorage if you need persistence (Demo 20's pattern), otherwise the checkbox-hack from Demo 08 gives you a zero-JS variant that resets on reload. SaaS admin is $8-25 CPM because the audience is engineering leaders and CTOs evaluating platform tools with 5-6 figure annual budgets. Every element of Demo 02 is production-tested against Vercel and Linear's actual shipped sidebars — including the tooltip delay curve.<aside aria-label="Main navigation"> so JAWS, NVDA, and VoiceOver announce it as a navigation region users can skip to. Two: real navigation structure. Use <nav><ul><li><a>, not <div onclick>. Screen readers announce list count so blind users know how many items are in the nav. Three: aria-current="page" on the active link — this is what tells assistive tech you are here, and CSS styles it via a[aria-current="page"] { background: var(--bg-active); }. Four: aria-expanded plus aria-controls on any toggle button, and aria-expanded is automatic on <summary> inside <details> — which is why Demo 22 and Demo 24 use native disclosure elements. Five: 44x44px minimum hit targets per WCAG 2.5.5 Target Size (AA) — nav a { min-block-size: 44px; padding-inline: 0.75rem; }. This is the single most-failed criterion in accessibility audits. Six: :focus-visible outlines with 3:1 contrast against adjacent colors per WCAG 2.4.13 Focus Appearance (AAA) and 2:1 minimum for AA — :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } where --focus is tested against both surface colors. Seven: prefers-reduced-motion guard on every animation per WCAG 2.3.3 Animation from Interactions (AAA) — @media (prefers-reduced-motion: reduce) { *, ::before, ::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }. Demos 01, 02, 08, and 22 are audited against these criteria. The legal exposure is real: US Section 508 of the Rehabilitation Act requires federal contractors to meet WCAG 2.0 AA; ADA Title III lawsuits against inaccessible websites hit 4,000+ filings per year; the EU Accessibility Act enforcement began June 2025 with fines up to €1M per member state; Canada's Accessible Canada Act mandates federal compliance; Australia's Disability Discrimination Act has been used in successful lawsuits (Maguire v SOCOG); and the UK Equality Act 2010 requires reasonable adjustments including web accessibility. Shipping these seven non-negotiables is not a checkbox — it is what keeps your company out of a demand letter. Compliance content is a $8-15 CPM vertical because the buyers are procurement teams, government contractors, and enterprise legal.class becomes className, for becomes htmlFor, tabindex becomes tabIndex, and self-closing tags need the trailing slash. Our HTML to JSX converter handles this in one paste. CSS ports even more cleanly: drop the .sbl-NN-scoped block into a CSS Module (Sidebar.module.css), a styled-components template literal, an Astro scoped <style>, a Svelte scoped <style>, or a Vue <style scoped> — the class scoping guarantees no bleed. For Demo 20's JS, the pattern is useEffect(() => { const saved = localStorage.getItem('sbl-collapsed'); }, []) in React, onMount in Svelte and Vue, or <script> tag in Astro. Critically, 29 of 30 demos are SSR-safe in Next.js App Router — they render on the server with zero hydration boundary, no 'use client' directive needed, and no client:only in Astro. This means your sidebar ships in the initial HTML payload, is interactive before JS loads (via :has(:checked), <details>, and :target), and costs literally 0KB of JavaScript. That is a lighthouse win no React sidebar library can match. On the ecosystem gap: shadcn/ui does not ship a sidebar primitive — it has NavigationMenu for horizontal nav and a community <Sidebar> block that requires Radix Collapsible plus 400 lines of React, so these 30 demos fill exactly that gap. Tailwind Catalyst ships a sidebar but at $299/developer/year — every demo here is MIT-licensed, copy-paste-free. For Tailwind conversion, run our CSS-to-Tailwind converter, though we recommend keeping the scoped CSS because Tailwind cannot express :has(:checked), @container, or interpolate-size cleanly. Framework support verified: Next.js 14+ App Router with Server Components, Astro 4+ static and island architecture, Nuxt 3, SvelteKit, Remix, SolidStart, Qwik City, plus vanilla HTML. There is no framework these break in — that is what framework-agnostic means.15 hand-coded CSS aspect-ratio demos for video embeds, product galleries, IAB ad slots, native dialogs, and CLS-safe placeholders: 16:9 iframe YouTube embed with click-to-load facade + up-next rail, 21:9 full-screen hero video with min-height/max-height:100svh guardrails + Ken Burns drift, 1:1 Instagram-style square grid with 2×2 feature tile, 4:3 magazine card image cover, 9:16 vertical stories rail (TikTok/Reels/Shorts pattern) with scroll-snap + segment bars, 4:5 apparel product image gallery with radio-driven state + 1:1 thumbnails, 1:1 zoom thumbnail carousel with pointer-tracked --zx/--zy magnifier, prevent-layout-shift placeholder with live PerformanceObserver CLS meter (before/after side-by-side lab), dynamic content fallback with three policies (bend/strict/auto) via :has() toggle, IAB ad banner slots (728×90 leaderboard, 300×250 medium rectangle, 300×600 half-page, 320×50 mobile) with skeletons + container-query mobile swap, responsive modal dialog sized min(92vw, 88svh×16/9, 1080px) with @starting-style animation, responsive OpenStreetMap iframe wrapper with 16:9 → 1:1 container-query narrow swap, media query orientation live HUD + resize:both container-ratio playground, object-fit logo cloud with contain/cover/fill switcher via :has(), and side-by-side padding-top hack (height:0 + padding-top:56.25%) → modern aspect-ratio migration comparison with live ratio picker. 11 truly Pure CSS + 4 with a tiny optional vanilla JS snippet (facade swap, pointer magnifier, CLS meter, native dialog). Every demo is scoped under a unique .car-NN prefix so it drops into any project without CSS collisions, ships prefers-reduced-motion guards, and is framework-agnostic (works as-is in React, Vue, Svelte, Astro, Next.js, Remix, or plain HTML).
6 production CSS bento grid layouts with 12 hand-coded designs — Apple-style product showcase, SaaS feature value-prop matrix, analytics dashboard overview, creative portfolio, e-commerce category hub, and link-in-bio social hub. Each topic ships 2 sub-variants side-by-side so visitors see both a keynote aesthetic and a paper-doc variant at once. Container queries, CSS Grid, zero dependencies, copy-paste ready.
20 hand-coded CSS blog and editorial layouts for magazine homepages, news sites, personal archives, newsletter back-issues, recipe and podcast blogs, developer tutorials, and long-form journalism. Covers a magazine homepage with hero plus editor-picks rail, a news homepage with breaking-news ticker, blog archives with sticky year separators, Substack-style issue-numbered archives, recipe card grids with prep-time badges, podcast episode lists with waveforms, long-form articles with scroll-driven reading progress, drop caps and floated pull quotes via first-letter, Medium-style 65ch centred reader layouts, dev tutorials with a sticky code panel beside prose, interview transcripts with alternating speakers, case-study pages with stat counters, video blogs with an episode rail, full-bleed photo essays, news articles with sticky related-stories sidebars, documentation pages with prev/next chapter nav, a standalone blog post card, a scroll-snap category filter bar, an inline newsletter callout that breaks out of the prose column, and a horizontal featured-post rail. 12 are pure CSS with zero JavaScript. Every layout is scoped under a .bl-NN prefix for no-collision pasting, guards prefers-reduced-motion, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.