Skip to content
UI ComponentSidebarNavigationNew

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
Workspace sidebar with a collapsible rail, nested project menus, storage meter and user profile

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

Already linked inside the component's files — no extra setup needed.


Desktop Preview


HTML

html
1<!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

css
1/* 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

javascript
1// ---------- 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

Browsers
ChromeEdgeFirefoxSafariOperaBraveArcVivaldi
Devices
DesktopTabletMobile
Frameworks
Vanilla JSHTML/CSS

❓ 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.

MetaSkillGroup

Written by

MetaSkillGroup

October 23, 2026

Guides from our blog

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.