Workspace Sidebar with Collapsible Rail & Flyout Menus
A complete app sidebar: workspace switcher, menu search with highlights, nested groups, a sliding active indicator, badges, drag-to-reorder favorites, a storage / upgrade card and a user menu with status. Collapse it into a 76px icon rail (or press [) — tooltips and flyout submenus take over. On phones it becomes an off-canvas drawer you can swipe closed. Everything is data-driven and remembered.
- Published 23 Oct 2026
Overview
Workspace Sidebar with Collapsible Rail & Flyout Menus
The sidebar every dashboard needs — collapses to a rail with flyouts, searches itself and remembers how you left it.
What's Included
- Expanded sidebar ↔ 76px icon rail (button or [ shortcut), remembered
- Tooltips and flyout submenus in the collapsed rail
- Workspace switcher and user menu with online / away / busy status
- Menu search with highlighted matches (press / to focus)
- Nested groups with smooth expand and a sliding active indicator
- Drag-to-reorder favorites, saved in localStorage
- Off-canvas drawer on phones with swipe-to-close
Dependencies
- Google Fonts — Onest https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700&display=swap
Already linked inside the component's files — no extra setup needed.
Desktop Preview
HTML
html1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>Workspace Sidebar | MetaSkillGroup</title> 7 <link rel="stylesheet" href="style.css" /> 8 </head> 9 <body> 10 <!-- created by metaskillgroup-team --> 11 <div class="app"> 12 <aside class="sidebar" id="sidebar" aria-label="Sidebar"> 13 <!-- Workspace switcher + collapse --> 14 <div class="sb-top"> 15 <div class="ws"> 16 <button type="button" class="ws-btn" id="ws-btn" aria-haspopup="menu" aria-expanded="false"> 17 <span class="ws-logo" id="ws-logo">A</span> 18 <span class="ws-text"> 19 <b id="ws-name">Acme Inc</b> 20 <small id="ws-plan">Pro · 12 members</small> 21 </span> 22 <svg class="ws-chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9l4-4 4 4M8 15l4 4 4-4" /></svg> 23 </button> 24 <div class="ws-menu" id="ws-menu" role="menu"></div> 25 </div> 26 <button type="button" class="collapse-btn" id="collapse-btn" aria-label="Collapse sidebar ( [ )" aria-pressed="false"> 27 <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="3" /><path d="M9 4v16M15 10l-2 2 2 2" /></svg> 28 </button> 29 </div> 30 31 <label class="sb-search"> 32 <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5" /></svg> 33 <input type="search" id="nav-search" placeholder="Search menu" aria-label="Search menu" /> 34 <kbd>/</kbd> 35 </label> 36 37 <!-- Navigation is rendered from script.js --> 38 <nav class="sb-nav" id="sb-nav" aria-label="Main"> 39 <span class="indicator" aria-hidden="true"></span> 40 </nav> 41 42 <!-- Storage / upgrade card --> 43 <div class="upgrade"> 44 <div class="up-head"> 45 <b>Storage</b> 46 <span id="storage-text">7.4 of 10 GB</span> 47 </div> 48 <div class="up-bar"><i id="storage-bar"></i></div> 49 <p>Upgrade to Business for 1 TB and unlimited guests.</p> 50 <a href="#" class="up-btn">Upgrade plan</a> 51 </div> 52 53 <!-- User --> 54 <div class="user"> 55 <button type="button" class="user-btn" id="user-btn" aria-haspopup="menu" aria-expanded="false"> 56 <span class="u-av">RS<i class="u-status" id="u-status" data-status="online"></i></span> 57 <span class="u-text"><b>Riya Sharma</b><small>riya@acme.com</small></span> 58 <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="12" cy="19" r="1.5" /></svg> 59 </button> 60 <div class="user-menu" id="user-menu" role="menu"> 61 <button type="button" role="menuitemradio" data-status="online" aria-checked="true"><i></i>Online</button> 62 <button type="button" role="menuitemradio" data-status="away" aria-checked="false"><i></i>Away</button> 63 <button type="button" role="menuitemradio" data-status="dnd" aria-checked="false"><i></i>Do not disturb</button> 64 <hr /> 65 <a href="#" role="menuitem">Account settings</a> 66 <a href="#" role="menuitem" class="danger">Log out</a> 67 </div> 68 </div> 69 </aside> 70 71 <div class="backdrop" id="backdrop"></div> 72 73 <!-- Main area --> 74 <div class="main"> 75 <header class="topbar"> 76 <button type="button" class="menu-btn" id="menu-btn" aria-label="Open sidebar" aria-controls="sidebar" aria-expanded="false"> 77 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10" /></svg> 78 </button> 79 <div class="crumbs" id="crumbs"></div> 80 <span class="shortcut">Press <kbd>[</kbd> to collapse</span> 81 </header> 82 <section class="content"> 83 <h1 id="page-title">Home</h1> 84 <p class="sub" id="page-sub">Your workspace at a glance.</p> 85 <div class="grid"> 86 <div class="card big"></div> 87 <div class="card"></div> 88 <div class="card"></div> 89 <div class="card"></div> 90 <div class="card wide"></div> 91 </div> 92 </section> 93 </div> 94 </div> 95 96 <!-- Shared tooltip + flyout used when the sidebar is collapsed --> 97 <div class="tooltip" id="tooltip" role="tooltip"></div> 98 <div class="flyout" id="flyout"></div> 99 100 <script src="script.js"></script> 101 </body> 102</html>
CSS
css1/* Importing Google Fonts - Onest */ 2@import url("https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700&display=swap"); 3 4* { 5 margin: 0; 6 padding: 0; 7 box-sizing: border-box; 8 font-family: "Onest", sans-serif; 9} 10 11:root { 12 --bg: #f4f4f6; 13 --side: #fbfbfc; 14 --surface: #ffffff; 15 --text: #1c1c22; 16 --muted: #6e6f7a; 17 --faint: #9a9ba5; 18 --border: #e7e7ec; 19 --hover: #efeff3; 20 --accent: #5b5bd6; 21 --accent-soft: #ececfc; 22 --w-open: 272px; 23 --w-closed: 76px; 24 --ease: cubic-bezier(0.2, 0.8, 0.2, 1); 25} 26 27body { 28 background: var(--bg); 29 color: var(--text); 30} 31 32a { 33 color: inherit; 34 text-decoration: none; 35} 36 37kbd { 38 padding: 1px 6px; 39 border: 1px solid var(--border); 40 border-bottom-width: 2px; 41 border-radius: 6px; 42 background: var(--surface); 43 color: var(--faint); 44 font-family: inherit; 45 font-size: 0.72rem; 46 font-weight: 600; 47} 48 49svg { 50 flex-shrink: 0; 51 fill: none; 52 stroke: currentColor; 53 stroke-width: 1.9; 54 stroke-linecap: round; 55 stroke-linejoin: round; 56} 57 58.app { 59 display: flex; 60 min-height: 100vh; 61} 62 63/* ========================================================= 64 Sidebar 65 ========================================================= */ 66.sidebar { 67 position: sticky; 68 top: 0; 69 z-index: 30; 70 display: flex; 71 flex-direction: column; 72 gap: 12px; 73 width: var(--w-open); 74 height: 100vh; 75 padding: 14px 12px; 76 border-right: 1px solid var(--border); 77 background: var(--side); 78 transition: width 0.35s var(--ease); 79} 80 81.sidebar.is-collapsed { 82 width: var(--w-closed); 83} 84 85/* Text that disappears in the collapsed rail */ 86.ws-text, 87.ws-chev, 88.label, 89.caret, 90.sb-search input, 91.sb-search kbd, 92.nav-title, 93.u-text, 94.user-btn > svg, 95.upgrade { 96 transition: opacity 0.2s; 97} 98 99.is-collapsed .ws-text, 100.is-collapsed .ws-chev, 101.is-collapsed .label, 102.is-collapsed .caret, 103.is-collapsed .sb-search input, 104.is-collapsed .sb-search kbd, 105.is-collapsed .u-text, 106.is-collapsed .user-btn > svg { 107 opacity: 0; 108 pointer-events: none; 109} 110 111.is-collapsed .upgrade, 112.is-collapsed .nav-badge { 113 display: none; 114} 115 116/* ---------- Top: workspace + collapse ---------- */ 117.sb-top { 118 display: flex; 119 align-items: center; 120 gap: 6px; 121} 122 123.ws { 124 position: relative; 125 flex: 1; 126 min-width: 0; 127} 128 129.ws-btn { 130 display: flex; 131 align-items: center; 132 gap: 10px; 133 width: 100%; 134 padding: 6px; 135 border: 0; 136 border-radius: 12px; 137 background: none; 138 color: var(--text); 139 text-align: left; 140 cursor: pointer; 141 transition: background 0.2s; 142} 143 144.ws-btn:hover, 145.ws-btn[aria-expanded="true"] { 146 background: var(--hover); 147} 148 149.ws-logo { 150 display: grid; 151 place-items: center; 152 flex-shrink: 0; 153 width: 34px; 154 height: 34px; 155 border-radius: 10px; 156 background: var(--ws, linear-gradient(135deg, #5b5bd6, #a855f7)); 157 color: #fff; 158 font-weight: 700; 159} 160 161.ws-text { 162 min-width: 0; 163 flex: 1; 164 white-space: nowrap; 165} 166 167.ws-text b { 168 display: block; 169 overflow: hidden; 170 font-size: 0.92rem; 171 text-overflow: ellipsis; 172} 173 174.ws-text small { 175 color: var(--muted); 176 font-size: 0.76rem; 177} 178 179.ws-chev { 180 width: 16px; 181 height: 16px; 182 color: var(--faint); 183} 184 185.collapse-btn { 186 display: grid; 187 place-items: center; 188 flex-shrink: 0; 189 width: 34px; 190 height: 34px; 191 border: 0; 192 border-radius: 10px; 193 background: none; 194 color: var(--muted); 195 cursor: pointer; 196 transition: background 0.2s, transform 0.35s var(--ease); 197} 198 199.collapse-btn:hover { 200 background: var(--hover); 201 color: var(--text); 202} 203 204.collapse-btn svg { 205 width: 20px; 206 height: 20px; 207} 208 209.is-collapsed .sb-top { 210 flex-direction: column; 211} 212 213.is-collapsed .ws { 214 flex: none; 215 width: 100%; 216} 217 218.is-collapsed .ws-btn { 219 justify-content: center; 220 padding: 6px 0; 221} 222 223.is-collapsed .ws-text, 224.is-collapsed .ws-chev, 225.is-collapsed .drag-hint { 226 display: none; 227} 228 229.is-collapsed .collapse-btn { 230 transform: rotate(180deg); 231} 232 233/* Menus (workspace + user) */ 234.ws-menu, 235.user-menu { 236 position: absolute; 237 left: 0; 238 z-index: 40; 239 width: 250px; 240 padding: 6px; 241 border: 1px solid var(--border); 242 border-radius: 14px; 243 background: var(--surface); 244 box-shadow: 0 24px 50px -20px rgba(28, 28, 34, 0.35); 245 opacity: 0; 246 visibility: hidden; 247 transform: translateY(-4px) scale(0.98); 248 transition: opacity 0.18s, transform 0.22s var(--ease), visibility 0.18s; 249} 250 251.ws-menu { 252 top: calc(100% + 6px); 253} 254 255.user-menu { 256 bottom: calc(100% + 6px); 257 transform-origin: bottom left; 258} 259 260.ws.is-open .ws-menu, 261.user.is-open .user-menu { 262 opacity: 1; 263 visibility: visible; 264 transform: none; 265} 266 267.ws-menu p { 268 padding: 8px 10px 4px; 269 color: var(--faint); 270 font-size: 0.72rem; 271 font-weight: 600; 272 text-transform: uppercase; 273 letter-spacing: 0.06em; 274} 275 276.ws-menu button, 277.ws-menu a, 278.user-menu button, 279.user-menu a { 280 display: flex; 281 align-items: center; 282 gap: 10px; 283 width: 100%; 284 padding: 8px 10px; 285 border: 0; 286 border-radius: 9px; 287 background: none; 288 color: var(--text); 289 font-size: 0.88rem; 290 text-align: left; 291 cursor: pointer; 292} 293 294.ws-menu button:hover, 295.ws-menu a:hover, 296.user-menu button:hover, 297.user-menu a:hover { 298 background: var(--hover); 299} 300 301.ws-menu .mini { 302 display: grid; 303 place-items: center; 304 width: 24px; 305 height: 24px; 306 border-radius: 7px; 307 color: #fff; 308 font-size: 0.72rem; 309 font-weight: 700; 310} 311 312.ws-menu [aria-checked="true"]::after { 313 content: "✓"; 314 margin-left: auto; 315 color: var(--accent); 316 font-weight: 700; 317} 318 319.ws-menu hr, 320.user-menu hr { 321 margin: 6px 4px; 322 border: 0; 323 border-top: 1px solid var(--border); 324} 325 326.user-menu .danger { 327 color: #dc2626; 328} 329 330.user-menu button i { 331 width: 8px; 332 height: 8px; 333 border-radius: 50%; 334 background: #22c55e; 335} 336 337.user-menu [data-status="away"] i { background: #f59e0b; } 338.user-menu [data-status="dnd"] i { background: #ef4444; } 339 340.user-menu [aria-checked="true"] { 341 font-weight: 600; 342} 343 344.user-menu [aria-checked="true"]::after { 345 content: "✓"; 346 margin-left: auto; 347 color: var(--accent); 348} 349 350/* ---------- Search ---------- */ 351.sb-search { 352 display: flex; 353 align-items: center; 354 gap: 8px; 355 height: 38px; 356 padding: 0 11px; 357 border: 1px solid var(--border); 358 border-radius: 11px; 359 background: var(--surface); 360 color: var(--faint); 361 cursor: text; 362} 363 364.sb-search:focus-within { 365 border-color: var(--accent); 366 box-shadow: 0 0 0 3px rgba(91, 91, 214, 0.15); 367} 368 369.sb-search svg { 370 width: 17px; 371 height: 17px; 372} 373 374.sb-search input { 375 flex: 1; 376 min-width: 0; 377 border: 0; 378 background: none; 379 color: var(--text); 380 font-size: 0.88rem; 381 outline: none; 382} 383 384.is-collapsed .sb-search { 385 justify-content: center; 386 padding: 0; 387 cursor: pointer; 388} 389 390.is-collapsed .sb-search input { 391 flex: 0; 392 width: 0; 393} 394 395.is-collapsed .sb-search kbd { 396 display: none; 397} 398 399/* Center the icons in the rail */ 400.is-collapsed .nav-link { 401 padding: 0 16px; 402} 403 404/* ---------- Nav ---------- */ 405.sb-nav { 406 position: relative; 407 flex: 1; 408 margin: 0 -6px; 409 padding: 0 6px; 410 overflow-y: auto; 411 overflow-x: hidden; 412 scrollbar-width: thin; 413} 414 415.indicator { 416 position: absolute; 417 left: 6px; 418 right: 6px; 419 top: 0; 420 z-index: 0; 421 height: var(--h, 0); 422 border-radius: 10px; 423 background: var(--accent-soft); 424 transform: translateY(var(--y, 0)); 425 transition: transform 0.35s var(--ease), height 0.35s var(--ease), opacity 0.2s; 426 pointer-events: none; 427} 428 429.indicator::before { 430 content: ""; 431 position: absolute; 432 left: -6px; 433 top: 8px; 434 bottom: 8px; 435 width: 3px; 436 border-radius: 0 3px 3px 0; 437 background: var(--accent); 438} 439 440.nav-section + .nav-section { 441 margin-top: 14px; 442} 443 444.nav-title { 445 display: flex; 446 justify-content: space-between; 447 align-items: center; 448 height: 24px; 449 padding: 0 10px; 450 color: var(--faint); 451 font-size: 0.72rem; 452 font-weight: 600; 453 letter-spacing: 0.06em; 454 text-transform: uppercase; 455 white-space: nowrap; 456} 457 458/* Section titles become thin dividers in the rail */ 459.is-collapsed .nav-title { 460 font-size: 0; 461 position: relative; 462} 463 464.is-collapsed .nav-title::after { 465 content: ""; 466 position: absolute; 467 left: 14px; 468 right: 14px; 469 top: 50%; 470 border-top: 1px solid var(--border); 471} 472 473.nav-list { 474 list-style: none; 475} 476 477.nav-link { 478 position: relative; 479 z-index: 1; 480 display: flex; 481 align-items: center; 482 gap: 11px; 483 width: 100%; 484 height: 38px; 485 padding: 0 10px; 486 border: 0; 487 border-radius: 10px; 488 background: none; 489 color: var(--muted); 490 font-size: 0.92rem; 491 font-weight: 500; 492 white-space: nowrap; 493 text-align: left; 494 cursor: pointer; 495 transition: color 0.2s, background 0.2s; 496} 497 498.nav-link:hover { 499 color: var(--text); 500 background: rgba(239, 239, 243, 0.7); 501} 502 503.nav-link.is-active { 504 color: var(--accent); 505 font-weight: 600; 506 background: none; 507} 508 509.nav-link:focus-visible, 510.collapse-btn:focus-visible, 511.ws-btn:focus-visible, 512.user-btn:focus-visible, 513.menu-btn:focus-visible { 514 outline: 2px solid var(--accent); 515 outline-offset: 1px; 516} 517 518.nav-link > svg { 519 width: 19px; 520 height: 19px; 521} 522 523.label { 524 flex: 1; 525 overflow: hidden; 526 text-overflow: ellipsis; 527} 528 529.label mark { 530 border-radius: 3px; 531 background: #fde68a; 532 color: inherit; 533} 534 535.nav-badge { 536 min-width: 22px; 537 padding: 1px 7px; 538 border-radius: 999px; 539 background: var(--hover); 540 color: var(--muted); 541 font-size: 0.72rem; 542 font-weight: 700; 543 text-align: center; 544} 545 546.nav-link.is-active .nav-badge { 547 background: var(--accent); 548 color: #fff; 549} 550 551/* In the rail, a badge becomes a dot on the icon */ 552.badge-dot { 553 position: absolute; 554 top: 8px; 555 left: 30px; 556 width: 8px; 557 height: 8px; 558 border: 2px solid var(--side); 559 border-radius: 50%; 560 background: #ef4444; 561 opacity: 0; 562 transition: opacity 0.2s; 563} 564 565.is-collapsed .badge-dot { 566 opacity: 1; 567} 568 569.caret { 570 width: 16px !important; 571 height: 16px !important; 572 color: var(--faint); 573 transition: transform 0.3s var(--ease), opacity 0.2s; 574} 575 576.nav-link[aria-expanded="true"] .caret { 577 transform: rotate(90deg); 578} 579 580/* Nested items */ 581.sub { 582 display: grid; 583 grid-template-rows: 0fr; 584 transition: grid-template-rows 0.3s var(--ease); 585} 586 587.sub > ul { 588 overflow: hidden; 589 list-style: none; 590} 591 592.nav-link[aria-expanded="true"] + .sub { 593 grid-template-rows: 1fr; 594} 595 596.is-collapsed .sub { 597 display: none; 598} 599 600.sub .nav-link { 601 height: 34px; 602 padding-left: 40px; 603 font-size: 0.88rem; 604} 605 606.sub .nav-link::before { 607 content: ""; 608 position: absolute; 609 left: 19px; 610 top: 0; 611 bottom: 0; 612 border-left: 1px solid var(--border); 613} 614 615/* Favorites */ 616.fav-dot { 617 width: 19px; 618 display: grid; 619 place-items: center; 620} 621 622.fav-dot i { 623 width: 10px; 624 height: 10px; 625 border-radius: 3px; 626 background: var(--c); 627} 628 629.fav-item.is-dragging { 630 opacity: 0.4; 631} 632 633.fav-item.drop-before { 634 box-shadow: 0 -2px 0 var(--accent); 635} 636 637.drag-hint { 638 font-size: 0.68rem; 639 text-transform: none; 640 letter-spacing: 0; 641} 642 643.nav-empty { 644 padding: 16px 10px; 645 color: var(--faint); 646 font-size: 0.85rem; 647} 648 649/* ---------- Upgrade card ---------- */ 650.upgrade { 651 padding: 14px; 652 border: 1px solid var(--border); 653 border-radius: 14px; 654 background: linear-gradient(160deg, var(--surface), #f3f2fe); 655} 656 657.up-head { 658 display: flex; 659 justify-content: space-between; 660 font-size: 0.84rem; 661} 662 663.up-head span { 664 color: var(--muted); 665} 666 667.up-bar { 668 height: 6px; 669 margin: 10px 0; 670 border-radius: 6px; 671 background: var(--hover); 672 overflow: hidden; 673} 674 675.up-bar i { 676 display: block; 677 width: 0; 678 height: 100%; 679 border-radius: inherit; 680 background: linear-gradient(90deg, var(--accent), #a855f7); 681 transition: width 1s var(--ease); 682} 683 684.upgrade p { 685 color: var(--muted); 686 font-size: 0.8rem; 687 line-height: 1.45; 688} 689 690.up-btn { 691 display: block; 692 margin-top: 10px; 693 padding: 8px; 694 border-radius: 9px; 695 background: var(--text); 696 color: #fff; 697 font-size: 0.84rem; 698 font-weight: 600; 699 text-align: center; 700} 701 702/* ---------- User ---------- */ 703.user { 704 position: relative; 705} 706 707.user-btn { 708 display: flex; 709 align-items: center; 710 gap: 10px; 711 width: 100%; 712 padding: 6px; 713 border: 0; 714 border-radius: 12px; 715 background: none; 716 color: var(--text); 717 text-align: left; 718 cursor: pointer; 719} 720 721.user-btn:hover, 722.user-btn[aria-expanded="true"] { 723 background: var(--hover); 724} 725 726.u-av { 727 position: relative; 728 display: grid; 729 place-items: center; 730 flex-shrink: 0; 731 width: 34px; 732 height: 34px; 733 border-radius: 50%; 734 background: linear-gradient(135deg, #f59e0b, #ef4444); 735 color: #fff; 736 font-size: 0.78rem; 737 font-weight: 700; 738} 739 740.u-status { 741 position: absolute; 742 right: -1px; 743 bottom: -1px; 744 width: 11px; 745 height: 11px; 746 border: 2px solid var(--side); 747 border-radius: 50%; 748 background: #22c55e; 749} 750 751.u-status[data-status="away"] { background: #f59e0b; } 752.u-status[data-status="dnd"] { background: #ef4444; } 753 754.u-text { 755 flex: 1; 756 min-width: 0; 757 white-space: nowrap; 758} 759 760.u-text b { 761 display: block; 762 font-size: 0.88rem; 763} 764 765.u-text small { 766 color: var(--muted); 767 font-size: 0.76rem; 768} 769 770.user-btn > svg { 771 width: 18px; 772 height: 18px; 773 color: var(--faint); 774} 775 776/* ---------- Tooltip + flyout (collapsed rail) ---------- */ 777.tooltip, 778.flyout { 779 position: fixed; 780 z-index: 60; 781 opacity: 0; 782 visibility: hidden; 783 transform: translateX(-4px); 784 transition: opacity 0.15s, transform 0.2s var(--ease), visibility 0.15s; 785} 786 787.tooltip { 788 padding: 6px 10px; 789 border-radius: 8px; 790 background: var(--text); 791 color: #fff; 792 font-size: 0.8rem; 793 font-weight: 600; 794 white-space: nowrap; 795 pointer-events: none; 796} 797 798.flyout { 799 min-width: 200px; 800 padding: 6px; 801 border: 1px solid var(--border); 802 border-radius: 14px; 803 background: var(--surface); 804 box-shadow: 0 24px 50px -20px rgba(28, 28, 34, 0.35); 805} 806 807.flyout b { 808 display: block; 809 padding: 8px 10px 6px; 810 font-size: 0.84rem; 811} 812 813.flyout button { 814 display: block; 815 width: 100%; 816 padding: 8px 10px; 817 border: 0; 818 border-radius: 9px; 819 background: none; 820 color: var(--muted); 821 font-size: 0.88rem; 822 text-align: left; 823 cursor: pointer; 824} 825 826.flyout button:hover, 827.flyout button.is-active { 828 background: var(--accent-soft); 829 color: var(--accent); 830} 831 832.tooltip.is-on, 833.flyout.is-on { 834 opacity: 1; 835 visibility: visible; 836 transform: none; 837} 838 839/* ========================================================= 840 Main area 841 ========================================================= */ 842.main { 843 flex: 1; 844 min-width: 0; 845} 846 847.topbar { 848 position: sticky; 849 top: 0; 850 z-index: 10; 851 display: flex; 852 align-items: center; 853 gap: 12px; 854 height: 60px; 855 padding: 0 28px; 856 border-bottom: 1px solid var(--border); 857 background: rgba(244, 244, 246, 0.85); 858 backdrop-filter: blur(10px); 859} 860 861.menu-btn { 862 display: none; 863 place-items: center; 864 width: 38px; 865 height: 38px; 866 border: 1px solid var(--border); 867 border-radius: 10px; 868 background: var(--surface); 869 color: var(--text); 870 cursor: pointer; 871} 872 873.menu-btn svg { 874 width: 20px; 875 height: 20px; 876} 877 878.crumbs { 879 display: flex; 880 align-items: center; 881 gap: 8px; 882 color: var(--muted); 883 font-size: 0.9rem; 884} 885 886.crumbs b { 887 color: var(--text); 888 font-weight: 600; 889} 890 891.shortcut { 892 margin-left: auto; 893 color: var(--faint); 894 font-size: 0.82rem; 895} 896 897.content { 898 padding: 34px 28px 60px; 899} 900 901.content h1 { 902 font-size: 1.9rem; 903 letter-spacing: -0.02em; 904 animation: fadeUp 0.35s var(--ease); 905} 906 907.content .sub { 908 margin: 6px 0 26px; 909 color: var(--muted); 910} 911 912@keyframes fadeUp { 913 from { 914 opacity: 0; 915 transform: translateY(6px); 916 } 917} 918 919.grid { 920 display: grid; 921 grid-template-columns: repeat(3, 1fr); 922 gap: 18px; 923} 924 925.card { 926 height: 170px; 927 border: 1px solid var(--border); 928 border-radius: 18px; 929 background: var(--surface); 930} 931 932.card.big { 933 grid-row: span 2; 934 height: auto; 935 background: linear-gradient(160deg, #ffffff, #efeffd); 936} 937 938.card.wide { 939 grid-column: span 3; 940 height: 220px; 941} 942 943/* ========================================================= 944 Mobile: off-canvas drawer 945 ========================================================= */ 946.backdrop { 947 position: fixed; 948 inset: 0; 949 z-index: 25; 950 background: rgba(28, 28, 34, 0.4); 951 opacity: 0; 952 visibility: hidden; 953 transition: opacity 0.3s, visibility 0.3s; 954} 955 956@media (max-width: 820px) { 957 .sidebar, 958 .sidebar.is-collapsed { 959 position: fixed; 960 left: 0; 961 width: min(var(--w-open), 86vw); 962 transform: translateX(-102%); 963 transition: transform 0.35s var(--ease); 964 box-shadow: 20px 0 50px -20px rgba(28, 28, 34, 0.3); 965 } 966 967 .sidebar.is-open { 968 transform: translateX(var(--drag, 0px)); 969 } 970 971 .sidebar.is-dragging { 972 transition: none; 973 } 974 975 .sidebar.is-open + .backdrop { 976 opacity: 1; 977 visibility: visible; 978 } 979 980 .collapse-btn, 981 .shortcut { 982 display: none; 983 } 984 985 .menu-btn { 986 display: grid; 987 } 988 989 .topbar, 990 .content { 991 padding-left: 16px; 992 padding-right: 16px; 993 } 994 995 .grid { 996 grid-template-columns: 1fr; 997 } 998 999 .card.big, 1000 .card.wide { 1001 grid-row: auto; 1002 grid-column: auto; 1003 height: 170px; 1004 } 1005} 1006 1007@media (prefers-reduced-motion: reduce) { 1008 *, 1009 *::before, 1010 *::after { 1011 animation: none !important; 1012 transition: none !important; 1013 } 1014}
JavaScript
javascript1// ---------- Data (edit freely) ---------- 2const ICONS = { 3 home: '<path d="M3 11l9-8 9 8v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"/>', 4 inbox: '<path d="M3 13l3-8h12l3 8v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/><path d="M3 13h5l1 3h6l1-3h5"/>', 5 check: '<rect x="3" y="3" width="18" height="18" rx="4"/><path d="M8 12l3 3 5-6"/>', 6 folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>', 7 calendar: '<rect x="3" y="5" width="18" height="16" rx="3"/><path d="M8 3v4M16 3v4M3 10h18"/>', 8 chart: '<path d="M4 19V9M10 19V5M16 19v-7M22 19H2"/>', 9 users: '<circle cx="9" cy="8" r="3.5"/><path d="M2 20a7 7 0 0 1 14 0M16 4.5a3.5 3.5 0 0 1 0 7M22 20a6 6 0 0 0-4-5.6"/>', 10 settings: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/>', 11 help: '<circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6V14M12 17.5v.01"/>', 12 caret: '<path d="M9 6l6 6-6 6"/>', 13}; 14 15const nav = [ 16 { 17 items: [ 18 { id: "home", label: "Home", icon: "home", sub: "Your workspace at a glance." }, 19 { id: "inbox", label: "Inbox", icon: "inbox", badge: 12, sub: "12 unread conversations." }, 20 { id: "tasks", label: "My tasks", icon: "check", badge: 3, sub: "3 tasks due this week." }, 21 ], 22 }, 23 { 24 title: "Workspace", 25 items: [ 26 { 27 id: "projects", label: "Projects", icon: "folder", 28 children: [ 29 { id: "p-web", label: "Website redesign" }, 30 { id: "p-app", label: "Mobile app" }, 31 { id: "p-mkt", label: "Marketing site" }, 32 ], 33 }, 34 { id: "calendar", label: "Calendar", icon: "calendar", sub: "Meetings, launches and deadlines." }, 35 { 36 id: "reports", label: "Reports", icon: "chart", 37 children: [ 38 { id: "r-overview", label: "Overview" }, 39 { id: "r-revenue", label: "Revenue" }, 40 { id: "r-retention", label: "Retention" }, 41 ], 42 }, 43 { id: "team", label: "Team", icon: "users", sub: "12 members, 3 guests." }, 44 ], 45 }, 46 { 47 title: "Favorites", 48 favorites: true, 49 items: [ 50 { id: "f-q4", label: "Q4 launch plan", color: "#f59e0b" }, 51 { id: "f-brand", label: "Brand guidelines", color: "#10b981" }, 52 { id: "f-hiring", label: "Hiring pipeline", color: "#ef4444" }, 53 { id: "f-okr", label: "Team OKRs", color: "#3b82f6" }, 54 ], 55 }, 56 { 57 items: [ 58 { id: "settings", label: "Settings", icon: "settings", sub: "Workspace, billing and members." }, 59 { id: "help", label: "Help & support", icon: "help", sub: "Docs, chat and status." }, 60 ], 61 }, 62]; 63 64const workspaces = [ 65 { name: "Acme Inc", plan: "Pro · 12 members", letter: "A", bg: "linear-gradient(135deg, #5b5bd6, #a855f7)" }, 66 { name: "Side Project", plan: "Free · 2 members", letter: "S", bg: "linear-gradient(135deg, #10b981, #06b6d4)" }, 67 { name: "Freelance", plan: "Pro · just you", letter: "F", bg: "linear-gradient(135deg, #f59e0b, #ef4444)" }, 68]; 69 70const STORAGE = { used: 7.4, total: 10 }; 71 72// ---------- Helpers ---------- 73const $ = (id) => document.getElementById(id); 74const sidebar = $("sidebar"); 75const navEl = $("sb-nav"); 76const indicator = navEl.querySelector(".indicator"); 77const mobile = window.matchMedia("(max-width: 820px)"); 78const store = { 79 get: (k, d) => { try { return JSON.parse(localStorage.getItem(k)) ?? d; } catch { return d; } }, 80 set: (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch { /* storage blocked */ } }, 81}; 82const svg = (name, cls = "") => `<svg class="${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[name]}</svg>`; 83 84// Restore saved order of favorites 85const favSection = nav.find((s) => s.favorites); 86const savedOrder = store.get("sb-fav-order", null); 87if (savedOrder) favSection.items.sort((a, b) => savedOrder.indexOf(a.id) - savedOrder.indexOf(b.id)); 88 89const allItems = () => nav.flatMap((s) => s.items.flatMap((i) => [i, ...(i.children ?? []).map((c) => ({ ...c, parent: i }))])); 90const findItem = (id) => allItems().find((i) => i.id === id); 91 92let activeId = store.get("sb-active", "home"); 93let collapsed = store.get("sb-collapsed", false); 94let query = ""; 95 96// ---------- Render navigation ---------- 97const linkHTML = (item) => { 98 const lead = item.color ? `<span class="fav-dot"><i style="--c:${item.color}"></i></span>` : svg(item.icon); 99 const badge = item.badge ? `<span class="nav-badge">${item.badge}</span><span class="badge-dot"></span>` : ""; 100 const label = query ? item.label.replace(new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "ig"), "<mark>$1</mark>") : item.label; 101 if (item.children) { 102 const open = store.get(`sb-open-${item.id}`, false) || item.children.some((c) => c.id === activeId) || Boolean(query); 103 return `<button type="button" class="nav-link" data-id="${item.id}" data-label="${item.label}" aria-expanded="${open}">${lead}<span class="label">${label}</span>${svg("caret", "caret")}</button> 104 <div class="sub"><ul>${item.children.map((c) => `<li><button type="button" class="nav-link" data-id="${c.id}" data-label="${c.label}">${matchLabel(c.label)}</button></li>`).join("")}</ul></div>`; 105 } 106 return `<button type="button" class="nav-link" data-id="${item.id}" data-label="${item.label}">${lead}<span class="label">${label}</span>${badge}</button>`; 107}; 108const matchLabel = (text) => 109 `<span class="label">${query ? text.replace(new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "ig"), "<mark>$1</mark>") : text}</span>`; 110 111const matches = (item) => { 112 if (!query) return true; 113 const q = query.toLowerCase(); 114 return item.label.toLowerCase().includes(q) || (item.children ?? []).some((c) => c.label.toLowerCase().includes(q)); 115}; 116 117const renderNav = () => { 118 const html = nav 119 .map((section) => { 120 const items = section.items.filter(matches); 121 if (!items.length) return ""; 122 const title = section.title ? `<p class="nav-title">${section.title}${section.favorites ? '<span class="drag-hint">drag to sort</span>' : ""}</p>` : ""; 123 return `<div class="nav-section">${title}<ul class="nav-list">${items 124 .map((i) => `<li class="${section.favorites ? "fav-item" : ""}" ${section.favorites && !query ? `draggable="true" data-fav="${i.id}"` : ""}>${linkHTML(i)}</li>`) 125 .join("")}</ul></div>`; 126 }) 127 .join(""); 128 navEl.innerHTML = `<span class="indicator" aria-hidden="true"></span>${html || '<p class="nav-empty">No menu items match.</p>'}`; 129 markActive(); 130}; 131 132// ---------- Active item + sliding indicator ---------- 133const placeIndicator = () => { 134 const ind = navEl.querySelector(".indicator"); 135 const link = navEl.querySelector(".nav-link.is-active"); 136 // A child inside a closed group (or the rail) highlights its parent instead 137 let target = link; 138 if (link && (link.closest(".sub") && (collapsed || link.closest(".sub").previousElementSibling.getAttribute("aria-expanded") !== "true"))) { 139 target = link.closest("li").parentElement.closest("li").querySelector(":scope > .nav-link"); 140 } 141 if (!target) return (ind.style.opacity = 0); 142 ind.style.opacity = 1; 143 // offsetTop is already measured from the nav (its nearest positioned parent) 144 ind.style.setProperty("--y", `${target.offsetTop}px`); 145 ind.style.setProperty("--h", `${target.offsetHeight}px`); 146}; 147 148const markActive = () => { 149 navEl.querySelectorAll(".nav-link").forEach((l) => { 150 const on = l.dataset.id === activeId; 151 l.classList.toggle("is-active", on); 152 on ? l.setAttribute("aria-current", "page") : l.removeAttribute("aria-current"); 153 }); 154 placeIndicator(); 155}; 156 157const updateMain = () => { 158 const item = findItem(activeId) ?? findItem("home"); 159 $("page-title").textContent = item.label; 160 $("page-sub").textContent = item.sub ?? (item.parent ? `Part of ${item.parent.label}.` : "A favorite page."); 161 $("crumbs").innerHTML = `${$("ws-name").textContent} <span>/</span> ${item.parent ? `${item.parent.label} <span>/</span> ` : ""}<b>${item.label}</b>`; 162 const h1 = $("page-title"); 163 h1.style.animation = "none"; 164 void h1.offsetWidth; 165 h1.style.animation = ""; 166}; 167 168const select = (id) => { 169 activeId = id; 170 store.set("sb-active", id); 171 markActive(); 172 updateMain(); 173 if (mobile.matches) setDrawer(false); 174}; 175 176navEl.addEventListener("click", (e) => { 177 const link = e.target.closest(".nav-link"); 178 if (!link) return; 179 const item = findItem(link.dataset.id); 180 if (item?.children) { 181 if (collapsed) return openFlyout(link, item); // rail: show submenu to the side 182 const open = link.getAttribute("aria-expanded") !== "true"; 183 link.setAttribute("aria-expanded", open); 184 store.set(`sb-open-${item.id}`, open); 185 return; 186 } 187 select(link.dataset.id); 188}); 189 190// Re-place the indicator whenever groups open/close or the sidebar resizes 191new ResizeObserver(placeIndicator).observe(navEl); 192navEl.addEventListener("transitionend", placeIndicator); 193 194// ---------- Search ---------- 195const search = $("nav-search"); 196search.addEventListener("input", () => { 197 query = search.value.trim(); 198 renderNav(); 199}); 200search.addEventListener("keydown", (e) => { 201 if (e.key === "Enter") navEl.querySelector(".nav-link:not([aria-expanded])")?.click(); 202 if (e.key === "Escape") { 203 search.value = ""; 204 query = ""; 205 renderNav(); 206 search.blur(); 207 } 208}); 209document.querySelector(".sb-search").addEventListener("click", () => { 210 if (collapsed) setCollapsed(false); 211 search.focus(); 212}); 213 214// ---------- Favorites drag & drop ---------- 215let dragId = null; 216navEl.addEventListener("dragstart", (e) => { 217 const li = e.target.closest("[data-fav]"); 218 if (!li) return; 219 dragId = li.dataset.fav; 220 li.classList.add("is-dragging"); 221 e.dataTransfer.effectAllowed = "move"; 222}); 223navEl.addEventListener("dragover", (e) => { 224 const li = e.target.closest("[data-fav]"); 225 if (!li || !dragId) return; 226 e.preventDefault(); 227 navEl.querySelectorAll(".drop-before").forEach((x) => x.classList.remove("drop-before")); 228 li.classList.add("drop-before"); 229}); 230navEl.addEventListener("drop", (e) => { 231 const li = e.target.closest("[data-fav]"); 232 if (!li || !dragId) return; 233 e.preventDefault(); 234 const items = favSection.items; 235 const from = items.findIndex((i) => i.id === dragId); 236 const [moved] = items.splice(from, 1); 237 items.splice(items.findIndex((i) => i.id === li.dataset.fav), 0, moved); 238 store.set("sb-fav-order", items.map((i) => i.id)); 239 renderNav(); 240}); 241navEl.addEventListener("dragend", () => { 242 dragId = null; 243 navEl.querySelectorAll(".is-dragging, .drop-before").forEach((x) => x.classList.remove("is-dragging", "drop-before")); 244}); 245 246// ---------- Collapse to an icon rail ---------- 247const collapseBtn = $("collapse-btn"); 248const setCollapsed = (value) => { 249 collapsed = value; 250 store.set("sb-collapsed", value); 251 sidebar.classList.toggle("is-collapsed", value && !mobile.matches); 252 collapseBtn.setAttribute("aria-pressed", value); 253 collapseBtn.setAttribute("aria-label", value ? "Expand sidebar ( [ )" : "Collapse sidebar ( [ )"); 254 hideFloating(); 255 setTimeout(placeIndicator, 360); 256}; 257collapseBtn.addEventListener("click", () => setCollapsed(!collapsed)); 258 259// Tooltip + flyout for the rail 260const tooltip = $("tooltip"); 261const flyout = $("flyout"); 262let flyoutTimer; 263 264const placeBeside = (el, anchor) => { 265 const r = anchor.getBoundingClientRect(); 266 el.style.left = `${sidebar.getBoundingClientRect().right + 8}px`; 267 el.style.top = `${Math.min(r.top + r.height / 2 - el.offsetHeight / 2, innerHeight - el.offsetHeight - 8)}px`; 268}; 269 270const openFlyout = (anchor, item) => { 271 clearTimeout(flyoutTimer); 272 tooltip.classList.remove("is-on"); 273 flyout.innerHTML = `<b>${item.label}</b>${item.children 274 .map((c) => `<button type="button" data-id="${c.id}" class="${c.id === activeId ? "is-active" : ""}">${c.label}</button>`) 275 .join("")}`; 276 placeBeside(flyout, anchor); 277 flyout.classList.add("is-on"); 278}; 279const hideFloating = () => { 280 tooltip.classList.remove("is-on"); 281 flyout.classList.remove("is-on"); 282}; 283 284navEl.addEventListener("pointerover", (e) => { 285 if (!collapsed || mobile.matches) return; 286 const link = e.target.closest(".nav-link"); 287 if (!link) return; 288 const item = findItem(link.dataset.id); 289 if (item?.children) return openFlyout(link, item); 290 flyout.classList.remove("is-on"); 291 tooltip.textContent = link.dataset.label + (item?.badge ? ` · ${item.badge}` : ""); 292 placeBeside(tooltip, link); 293 tooltip.classList.add("is-on"); 294}); 295navEl.addEventListener("pointerleave", () => { 296 tooltip.classList.remove("is-on"); 297 flyoutTimer = setTimeout(() => flyout.classList.remove("is-on"), 250); 298}); 299flyout.addEventListener("pointerenter", () => clearTimeout(flyoutTimer)); 300flyout.addEventListener("pointerleave", () => flyout.classList.remove("is-on")); 301flyout.addEventListener("click", (e) => { 302 const b = e.target.closest("button"); 303 if (!b) return; 304 select(b.dataset.id); 305 flyout.classList.remove("is-on"); 306}); 307 308// ---------- Workspace switcher ---------- 309const wsWrap = document.querySelector(".ws"); 310const wsMenu = $("ws-menu"); 311let wsIndex = store.get("sb-ws", 0); 312const applyWorkspace = () => { 313 const w = workspaces[wsIndex]; 314 $("ws-name").textContent = w.name; 315 $("ws-plan").textContent = w.plan; 316 $("ws-logo").textContent = w.letter; 317 $("ws-logo").style.setProperty("--ws", w.bg); 318 wsMenu.innerHTML = `<p>Workspaces</p>${workspaces 319 .map((x, i) => `<button type="button" role="menuitemradio" aria-checked="${i === wsIndex}" data-ws="${i}"><span class="mini" style="background:${x.bg}">${x.letter}</span>${x.name}</button>`) 320 .join("")}<hr /><a href="#" role="menuitem">+ Create workspace</a><a href="#" role="menuitem">⚙ Workspace settings</a>`; 321 updateMain(); 322}; 323wsMenu.addEventListener("click", (e) => { 324 const b = e.target.closest("[data-ws]"); 325 if (!b) return; 326 wsIndex = Number(b.dataset.ws); 327 store.set("sb-ws", wsIndex); 328 applyWorkspace(); 329 toggleMenu(wsWrap, false); 330}); 331 332// ---------- User menu (status) ---------- 333const userWrap = document.querySelector(".user"); 334document.querySelectorAll(".user-menu [data-status]").forEach((b) => 335 b.addEventListener("click", () => { 336 document.querySelectorAll(".user-menu [data-status]").forEach((x) => x.setAttribute("aria-checked", x === b)); 337 $("u-status").dataset.status = b.dataset.status; 338 toggleMenu(userWrap, false); 339 }), 340); 341 342const toggleMenu = (wrap, open) => { 343 wrap.classList.toggle("is-open", open); 344 wrap.querySelector("button").setAttribute("aria-expanded", open); 345}; 346[[wsWrap, $("ws-btn")], [userWrap, $("user-btn")]].forEach(([wrap, btn]) => 347 btn.addEventListener("click", (e) => { 348 e.stopPropagation(); 349 const open = !wrap.classList.contains("is-open"); 350 toggleMenu(wsWrap, false); 351 toggleMenu(userWrap, false); 352 toggleMenu(wrap, open); 353 }), 354); 355document.addEventListener("click", (e) => { 356 if (!e.target.closest(".ws, .user")) { 357 toggleMenu(wsWrap, false); 358 toggleMenu(userWrap, false); 359 } 360}); 361 362// ---------- Mobile drawer (with swipe to close) ---------- 363const menuBtn = $("menu-btn"); 364const setDrawer = (open) => { 365 sidebar.classList.toggle("is-open", open); 366 menuBtn.setAttribute("aria-expanded", open); 367 document.body.style.overflow = open ? "hidden" : ""; 368}; 369menuBtn.addEventListener("click", () => setDrawer(true)); 370$("backdrop").addEventListener("click", () => setDrawer(false)); 371 372let startX = null; 373sidebar.addEventListener("pointerdown", (e) => { 374 if (!mobile.matches || e.pointerType === "mouse") return; 375 startX = e.clientX; 376}); 377sidebar.addEventListener("pointermove", (e) => { 378 if (startX === null) return; 379 const dx = Math.min(0, e.clientX - startX); 380 sidebar.classList.add("is-dragging"); 381 sidebar.style.setProperty("--drag", `${dx}px`); 382}); 383const endSwipe = (e) => { 384 if (startX === null) return; 385 const dx = e.clientX - startX; 386 startX = null; 387 sidebar.classList.remove("is-dragging"); 388 sidebar.style.removeProperty("--drag"); 389 if (dx < -70) setDrawer(false); 390}; 391sidebar.addEventListener("pointerup", endSwipe); 392sidebar.addEventListener("pointercancel", endSwipe); 393 394mobile.addEventListener("change", () => { 395 setDrawer(false); 396 setCollapsed(collapsed); 397}); 398 399// ---------- Keyboard shortcuts: [ to collapse, / to search, Esc to close ---------- 400document.addEventListener("keydown", (e) => { 401 const typing = /INPUT|TEXTAREA/.test(document.activeElement.tagName); 402 if (e.key === "[" && !typing && !mobile.matches) setCollapsed(!collapsed); 403 if (e.key === "/" && !typing) { 404 e.preventDefault(); 405 if (mobile.matches) setDrawer(true); 406 if (collapsed) setCollapsed(false); 407 search.focus(); 408 } 409 if (e.key === "Escape") { 410 toggleMenu(wsWrap, false); 411 toggleMenu(userWrap, false); 412 hideFloating(); 413 if (sidebar.classList.contains("is-open")) setDrawer(false); 414 } 415}); 416 417// ---------- Init ---------- 418$("storage-text").textContent = `${STORAGE.used} of ${STORAGE.total} GB`; 419requestAnimationFrame(() => ($("storage-bar").style.width = `${(STORAGE.used / STORAGE.total) * 100}%`)); 420renderNav(); 421applyWorkspace(); 422setCollapsed(collapsed); 423document.fonts?.ready.then(placeIndicator);
Built by MetaSkillGroup — copy it, use it, make it yours.
Features
Expanded sidebar ↔ 76px icon rail (button or [ shortcut), remembered
Tooltips and flyout submenus in the collapsed rail
Workspace switcher and user menu with online / away / busy status
Menu search with highlighted matches (press / to focus)
Nested groups with smooth expand and a sliding active indicator
Drag-to-reorder favorites, saved in localStorage
Off-canvas drawer on phones with swipe-to-close
Built With
HTML5
CSS3
JavaScript
Compatibility
❓ FAQs
Edit the nav array in script.js. Each section has an optional title and items with id, label, icon and optional badge or children for a nested group.
In localStorage: collapsed state, active page, open groups, favorites order and workspace. Clear the sb-* keys to reset.
Yes. Below 820px the sidebar becomes an off-canvas drawer opened from the top bar; tap the backdrop, press Esc or swipe left to close it.

Written by
MetaSkillGroup
Guides from our blog
Frontend Developer Roadmap 2026: A Step-by-Step Guide for BeginnersA simple, step-by-step frontend developer roadmap for 2026. Learn HTML, CSS, JavaScript, Git, React, TypeScript and Next.js in the right order, with a 6-month study plan, beginner projects, free resources and a checklist for your first frontend internship.
MERN Stack Roadmap for Beginners 2026: A Step-by-Step Guide to Becoming a Full-Stack DeveloperConfused about where to start with MERN? This step-by-step MERN stack roadmap for 2026 takes you from zero to your first full-stack project. Learn JavaScript, Node.js, Express, MongoDB and React in the right order, with a clear timeline, milestone projects, common mistakes and tips to learn faster with AI tools.
Top 7 React Optimization Techniques to Boost Your App Performance in 2026.Learn the top 7 React optimization techniques including Lazy Loading, Memoization, Debouncing, Throttling, Virtualization, Code Splitting, and Caching to build blazing fast React apps in 2026.
Need it built for you?
Built to the same standard as our client work.
If nothing here fits exactly, tell us what you need — our core developers build it. Requirements call first, a written SRS before any code, and a price agreed up front.