30 CSS Sidebar Layouts
23 / 30

All 30 in 30 CSS Sidebar Layouts

  1. 01Modern SaaS Dashboard Sidebar Menu with Native Dark Mode Switch
  2. 02Accessible Admin Panel Sidebar with CSS Tooltips and Icon Collapse
  3. 03Developer Documentation Sidebar with Scroll-Synced Nesting
  4. 04Mobile-First Responsive Sidebar to Bottom Navigation Switch Layout
  5. 05Data Analytics Sidebar Layout with CSS Subgrid Alignment
  6. 06Smooth Animated Collapsible Sidebar Menu with Icon-Only Mode
  7. 07CSS Position Sticky Sidebar Layout for E-Commerce and Blogs
  8. 08Pure CSS Collapsible Sidebar Drawer Menu without JavaScript
  9. 09CSS Fixed Sidebar Layout with Independent Scrollable Main Content
  10. 10Off-Canvas Navigation Drawer for Mobile & Desktop Web Apps
  11. 11Right-Aligned CSS Sidebar Layout for Secondary Tool Panels
  12. 12Two-Pane Webmail UI Layout with CSS Flexbox & Grid
  13. 13Dual Sidebar App Shell Layout with Left Nav and Right Context Panel
  14. 14Compact Mini Icon Rail Sidebar Navigation for SaaS Web Apps
  15. 15Responsive E-Commerce Product Filter Sidebar with Native Accordion Menu
  16. 16Modern Floating Glassmorphism Sidebar UI Component
  17. 17CSS Sidebar Menu with Sticky User Profile Footer
  18. 18Push Content Sidebar Navigation Layout with Smooth CSS Transitions
  19. 19Off-Canvas Sidebar Overlay Layout with Blur Backdrop
  20. 20Persistent Collapsible State Sidebar with CSS Variables
  21. 21App Sidebar Navigation with Integrated Sticky Search Bar
  22. 22Section-Grouped Sidebar Navigation Menu with Header Labels
  23. 23CSS Target Active Indicator Sidebar Menu without JavaScript
  24. 24Multi-Level Nested Tree Sidebar Navigation Layout
  25. 25Multi-Tab Sidebar UI Component with Dynamic Panel Switching
  26. 26Onboarding Wizard Sidebar Layout with Step Progress Tracking
  27. 27Pure CSS Resizable Sidebar Layout Using Resize Property
  28. 28Three-Pane Productivity Workspace Shell Layout
  29. 29Next-Gen Adaptive Sidebar Layout Using CSS Container Queries
  30. 30Frosted Glass Floating Sidebar Layout with Backdrop Blur
Pure CSSMIT licensed

CSS Target Active Indicator Sidebar Menu without JavaScript

A sliding active-indicator bar that animates between menu items when you click — the signature 'framer-motion layoutId' effect — with zero JavaScript. The links target page anchors, :target identifies the active section, and the indicator is a single element whose position transitions between :has()-selected states.

Live Demo
Plain CSSTailwind v4
Try it

Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69

Cruip · Mosaic by Cruip · Affiliate link

View Mosaic dashboard

The code

<section class="sbl-23">
  <aside class="sbl-23__rail" aria-label="Sections">
    <span class="sbl-23__bar" aria-hidden="true"></span>
    <nav>
      <a href="#sbl23-inbox">Inbox</a><a href="#sbl23-today">Today</a><a href="#sbl23-upcoming">Upcoming</a><a href="#sbl23-someday">Someday</a><a href="#sbl23-logbook">Logbook</a>
    </nav>
  </aside>
  <div class="sbl-23__panes">
    <section id="sbl23-inbox"><h1>Inbox</h1><p>Click the menu — the accent bar glides between items. URL fragments + :target + :has(), no script.</p></section>
    <section id="sbl23-today"><h1>Today</h1><p>Three tasks scheduled.</p></section>
    <section id="sbl23-upcoming"><h1>Upcoming</h1><p>Planning horizon: two weeks.</p></section>
    <section id="sbl23-someday"><h1>Someday</h1><p>Ideas parked without guilt.</p></section>
    <section id="sbl23-logbook"><h1>Logbook</h1><p>Completed items, newest first.</p></section>
  </div>
</section>
.sbl-23,
.sbl-23 *,
.sbl-23 *::before,
.sbl-23 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sbl-23 {
  --row: 46px;
  --panel: oklch(0.17 0.015 145);
  --ink: oklch(0.94 0.005 145);
  --mut: oklch(0.64 0.012 145);
  --line: oklch(0.28 0.016 145);
  --accent: oklch(0.8 0.16 150);
  display: grid;
  grid-template-columns: 224px 1fr;
  height: 100vh;
  height: 100dvh;
  font-family: system-ui,'Segoe UI',sans-serif;
  background: oklch(0.13 0.012 145);
  color: var(--ink);
}

.sbl-23__rail {
  position: relative;
  background: var(--panel);
  border-right: 1px solid var(--line);
  padding: 18px 12px;
}

.sbl-23__rail nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sbl-23__rail a {
  display: flex;
  align-items: center;
  height: 44px;
  padding: 0 16px;
  border-radius: 10px;
  font: 500 14px/1 system-ui,sans-serif;
  color: var(--mut);
  text-decoration: none;
  transition: color .2s ease,background-color .2s ease;
}

.sbl-23__rail a:hover {
  color: var(--ink);
  background: color-mix(in oklab,var(--ink) 6%,transparent);
}

.sbl-23__rail a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.sbl-23__bar {
  position: absolute;
  left: 4px;
  top: 18px;
  width: 3px;
  height: 28px;
  margin-top: 8px;
  border-radius: 4px;
  background: var(--accent);
  box-shadow: 0 0 12px color-mix(in oklab,var(--accent) 55%,transparent);
  translate: 0 0;
  transition: translate .32s cubic-bezier(.5,0,.2,1.15);
}

.sbl-23:has(#sbl23-today:target) .sbl-23__bar {
  translate: 0 var(--row);
}

.sbl-23:has(#sbl23-upcoming:target) .sbl-23__bar {
  translate: 0 calc(var(--row) * 2);
}

.sbl-23:has(#sbl23-someday:target) .sbl-23__bar {
  translate: 0 calc(var(--row) * 3);
}

.sbl-23:has(#sbl23-logbook:target) .sbl-23__bar {
  translate: 0 calc(var(--row) * 4);
}

.sbl-23:not(:has(:target)) a[href="#sbl23-inbox"],
.sbl-23:has(#sbl23-inbox:target) a[href="#sbl23-inbox"],
.sbl-23:has(#sbl23-today:target) a[href="#sbl23-today"],
.sbl-23:has(#sbl23-upcoming:target) a[href="#sbl23-upcoming"],
.sbl-23:has(#sbl23-someday:target) a[href="#sbl23-someday"],
.sbl-23:has(#sbl23-logbook:target) a[href="#sbl23-logbook"] {
  color: var(--ink);
  background: color-mix(in oklab,var(--accent) 12%,transparent);
}

.sbl-23__panes {
  overflow-y: auto;
  padding: 32px;
  display: grid;
  gap: 40px;
  align-content: start;
}

.sbl-23__panes section {
  scroll-margin-top: 24px;
}

.sbl-23__panes h1 {
  font: 600 21px/1.2 system-ui,sans-serif;
  margin-bottom: 8px;
}

.sbl-23__panes p {
  color: var(--mut);
  font-size: 14px;
  line-height: 1.6;
  max-width: 48ch;
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .sbl-23__bar,
    .sbl-23__rail a {
    transition: none;
  }
}
/* No JavaScript — :target marks the active section, :has() repositions one indicator bar via translate stops, and the transition makes it glide. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Sidebar Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Target Active Indicator Sidebar Menu without JavaScript
Source: https://codefronts.com/layouts/css-sidebar-layouts/target-active-highlight/

A sliding active-indicator bar that animates between menu items when you click — the signature 'framer-motion layoutId' effect — with zero JavaScript. The links target page anchors, :target identifies the active section, and the indicator is a single element whose position transitions between :has()-selected states.
## HTML
```html
<section class="sbl-23">
  <aside class="sbl-23__rail" aria-label="Sections">
    <span class="sbl-23__bar" aria-hidden="true"></span>
    <nav>
      <a href="#sbl23-inbox">Inbox</a><a href="#sbl23-today">Today</a><a href="#sbl23-upcoming">Upcoming</a><a href="#sbl23-someday">Someday</a><a href="#sbl23-logbook">Logbook</a>
    </nav>
  </aside>
  <div class="sbl-23__panes">
    <section id="sbl23-inbox"><h1>Inbox</h1><p>Click the menu — the accent bar glides between items. URL fragments + :target + :has(), no script.</p></section>
    <section id="sbl23-today"><h1>Today</h1><p>Three tasks scheduled.</p></section>
    <section id="sbl23-upcoming"><h1>Upcoming</h1><p>Planning horizon: two weeks.</p></section>
    <section id="sbl23-someday"><h1>Someday</h1><p>Ideas parked without guilt.</p></section>
    <section id="sbl23-logbook"><h1>Logbook</h1><p>Completed items, newest first.</p></section>
  </div>
</section>
```
## CSS
```css
.sbl-23,
.sbl-23 *,
.sbl-23 *::before,
.sbl-23 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sbl-23 {
  --row: 46px;
  --panel: oklch(0.17 0.015 145);
  --ink: oklch(0.94 0.005 145);
  --mut: oklch(0.64 0.012 145);
  --line: oklch(0.28 0.016 145);
  --accent: oklch(0.8 0.16 150);
  display: grid;
  grid-template-columns: 224px 1fr;
  height: 100vh;
  height: 100dvh;
  font-family: system-ui,'Segoe UI',sans-serif;
  background: oklch(0.13 0.012 145);
  color: var(--ink);
}

.sbl-23__rail {
  position: relative;
  background: var(--panel);
  border-right: 1px solid var(--line);
  padding: 18px 12px;
}

.sbl-23__rail nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sbl-23__rail a {
  display: flex;
  align-items: center;
  height: 44px;
  padding: 0 16px;
  border-radius: 10px;
  font: 500 14px/1 system-ui,sans-serif;
  color: var(--mut);
  text-decoration: none;
  transition: color .2s ease,background-color .2s ease;
}

.sbl-23__rail a:hover {
  color: var(--ink);
  background: color-mix(in oklab,var(--ink) 6%,transparent);
}

.sbl-23__rail a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.sbl-23__bar {
  position: absolute;
  left: 4px;
  top: 18px;
  width: 3px;
  height: 28px;
  margin-top: 8px;
  border-radius: 4px;
  background: var(--accent);
  box-shadow: 0 0 12px color-mix(in oklab,var(--accent) 55%,transparent);
  translate: 0 0;
  transition: translate .32s cubic-bezier(.5,0,.2,1.15);
}

.sbl-23:has(#sbl23-today:target) .sbl-23__bar {
  translate: 0 var(--row);
}

.sbl-23:has(#sbl23-upcoming:target) .sbl-23__bar {
  translate: 0 calc(var(--row) * 2);
}

.sbl-23:has(#sbl23-someday:target) .sbl-23__bar {
  translate: 0 calc(var(--row) * 3);
}

.sbl-23:has(#sbl23-logbook:target) .sbl-23__bar {
  translate: 0 calc(var(--row) * 4);
}

.sbl-23:not(:has(:target)) a[href="#sbl23-inbox"],
.sbl-23:has(#sbl23-inbox:target) a[href="#sbl23-inbox"],
.sbl-23:has(#sbl23-today:target) a[href="#sbl23-today"],
.sbl-23:has(#sbl23-upcoming:target) a[href="#sbl23-upcoming"],
.sbl-23:has(#sbl23-someday:target) a[href="#sbl23-someday"],
.sbl-23:has(#sbl23-logbook:target) a[href="#sbl23-logbook"] {
  color: var(--ink);
  background: color-mix(in oklab,var(--accent) 12%,transparent);
}

.sbl-23__panes {
  overflow-y: auto;
  padding: 32px;
  display: grid;
  gap: 40px;
  align-content: start;
}

.sbl-23__panes section {
  scroll-margin-top: 24px;
}

.sbl-23__panes h1 {
  font: 600 21px/1.2 system-ui,sans-serif;
  margin-bottom: 8px;
}

.sbl-23__panes p {
  color: var(--mut);
  font-size: 14px;
  line-height: 1.6;
  max-width: 48ch;
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .sbl-23__bar,
    .sbl-23__rail a {
    transition: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — :target marks the active section, :has() repositions one indicator bar via translate stops, and the transition makes it glide. */
```

How this works

Every link points at a section anchor (href="#sbl23-inbox"). Clicking updates the URL fragment, the matching section becomes :target, and the shell's :has() rules translate that into both a highlighted link AND a repositioned indicator. The indicator is one absolutely-positioned 3px bar; each state rule moves it with translate:0 Ypx where Y is the link's offset (rows are a fixed 44px, so item n = n×46px including gap). Because translate transitions, the bar GLIDES from the old item to the new one — the shared-element illusion, in one transition declaration.

The fixed-row-height contract is what makes the math clean: the indicator's positions are knowable constants, not measured values. That's the honest limitation and the honest lesson — this pattern fits STABLE menus (app sections, settings pages), while dynamic lists need demo 14's anchor positioning, which measures for you. Note the default state: with no fragment yet, a :not(:has(:target)) rule parks the indicator on item one, so first paint looks intentional.

Make it yours

  • Row height is the one constant to keep in sync — extract it as --row:46px and write the translate stops as calc(n * var(--row)).
  • Horizontal variant: same technique with translate X stops under a top tab bar.
  • Fatter pill indicator: swap the 3px bar for a full-row rounded rect at low opacity behind the links.
  • Add scroll-margin-top to the target sections so anchored jumps land with breathing room.

Gotchas — read before shipping

  • Every :target jump SCROLLS the content pane to the section — that's the feature, but wrap the pane in its own scroll context (as here) so the page itself doesn't jump.
  • One :target per document: if another widget on the page also uses fragments, they fight — namespace your IDs.
  • The no-fragment default state needs explicit handling or the indicator is invisible until the first click.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 105+.

:has() with :target inside it is the gate — baseline since Dec 2023. Fallback: links still highlight via :target styling, minus the glide.

Techniques used in this demo

Search CodeFronts

Loading…