07 / 20
All 20 in 20 CSS Sidebar Navigation
- 01Accessible Sidebar Navigation ARIA Reference
- 02Sidebar Focus Trap and Restore on Close
- 03Inert Background Content Behind a Drawer
- 04Sidebar Navigation Is Not a Tab List
- 05Announcing Collapsed and Expanded Sidebar State
- 06Roving Tabindex for Sidebar Keyboard Navigation
- 07Persisting Sidebar Collapse State Across Reloads
- 08Sidebar Scroll Containment with Overscroll Behavior
- 09Nested Submenu Keyboard and Screen Reader Behavior
- 10Resizable Sidebar with a Keyboard Accessible Handle
- 11Collapsible Sidebar with Checkbox State
- 12Responsive Sidebar Breakpoint Behavior
- 13Icon Rail Sidebar with Accessible Labels
- 14Dashboard Sidebar with Current Page State
- 15Multi Level Sidebar Menu Disclosure Pattern
- 16Accordion Sidebar with Details and Summary
- 17Sticky Full Height Sidebar Layout Mechanics
- 18Floating Sidebar Stacking and Overlay Order
- 19Off Canvas Sidebar Drawer with Focus Return
- 20Sidebar Footer Pinning with Scrollable Nav
Persisting Sidebar Collapse State Across Reloads
A sidebar that forgets it was collapsed is a small daily annoyance that compounds. Persist the boolean, then apply it before first paint — an inline script in <head> that sets a class on the root, the same trick a theme toggle uses. Reading the value in a framework effect instead means the wide sidebar paints first and visibly snaps shut. Every read and write is wrapped, because storage throws in sandboxed and private contexts.
Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69
Cruip · Mosaic by Cruip · Affiliate link
The code
<section class="sdb-07" aria-label="Prism analytics">
<div class="sdb-07__theme">
<input class="sdb-07__theme-input" type="checkbox" id="sdb-07-theme">
<label class="sdb-07__theme-label" for="sdb-07-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sdb-07__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-07__app" data-sdb-07-collapsed="false" data-sdb-07-noanim>
<aside class="sdb-07__rail" id="sdb-07-rail">
<div class="sdb-07__rail-head">
<span class="sdb-07__mark" aria-hidden="true">PR</span>
<p class="sdb-07__brand"><span class="sdb-07__hideable">Prism</span></p>
<button class="sdb-07__collapse" type="button" id="sdb-07-toggle" aria-expanded="true" aria-controls="sdb-07-rail">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h10M4 18h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span class="sdb-07__sr">Collapse sidebar</span>
</button>
</div>
<nav class="sdb-07__nav" aria-label="Prism reports">
<ul class="sdb-07__list">
<li><a class="sdb-07__link" href="#sdb-07-main" aria-current="page"><span class="sdb-07__ico" aria-hidden="true">01</span><span class="sdb-07__hideable">Overview</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">02</span><span class="sdb-07__hideable">Acquisition</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">03</span><span class="sdb-07__hideable">Retention</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">04</span><span class="sdb-07__hideable">Revenue</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">05</span><span class="sdb-07__hideable">Audit log</span></a></li>
</ul>
</nav>
</aside>
<main class="sdb-07__main" id="sdb-07-main">
<div class="sdb-07__kpis">
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Active workspaces</p><p class="sdb-07__kpi-v">1,284</p></div>
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Weekly retention</p><p class="sdb-07__kpi-v">71.4%</p></div>
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Net revenue</p><p class="sdb-07__kpi-v">$418k</p></div>
</div>
<div class="sdb-07__chart" aria-hidden="true">
<span style="--sdb-07-h:32"></span><span style="--sdb-07-h:48"></span><span style="--sdb-07-h:41"></span>
<span style="--sdb-07-h:66"></span><span style="--sdb-07-h:58"></span><span style="--sdb-07-h:74"></span>
<span style="--sdb-07-h:69"></span><span style="--sdb-07-h:88"></span><span style="--sdb-07-h:79"></span>
</div>
</main>
</div>
</section><section class="sdb-07" aria-label="Prism analytics">
<div class="sdb-07__theme">
<input class="sdb-07__theme-input" type="checkbox" id="sdb-07-theme">
<label class="sdb-07__theme-label" for="sdb-07-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sdb-07__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-07__app" data-sdb-07-collapsed="false" data-sdb-07-noanim>
<aside class="sdb-07__rail" id="sdb-07-rail">
<div class="sdb-07__rail-head">
<span class="sdb-07__mark" aria-hidden="true">PR</span>
<p class="sdb-07__brand"><span class="sdb-07__hideable">Prism</span></p>
<button class="sdb-07__collapse" type="button" id="sdb-07-toggle" aria-expanded="true" aria-controls="sdb-07-rail">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h10M4 18h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span class="sdb-07__sr">Collapse sidebar</span>
</button>
</div>
<nav class="sdb-07__nav" aria-label="Prism reports">
<ul class="sdb-07__list">
<li><a class="sdb-07__link" href="#sdb-07-main" aria-current="page"><span class="sdb-07__ico" aria-hidden="true">01</span><span class="sdb-07__hideable">Overview</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">02</span><span class="sdb-07__hideable">Acquisition</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">03</span><span class="sdb-07__hideable">Retention</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">04</span><span class="sdb-07__hideable">Revenue</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">05</span><span class="sdb-07__hideable">Audit log</span></a></li>
</ul>
</nav>
</aside>
<main class="sdb-07__main" id="sdb-07-main">
<div class="sdb-07__kpis">
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Active workspaces</p><p class="sdb-07__kpi-v">1,284</p></div>
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Weekly retention</p><p class="sdb-07__kpi-v">71.4%</p></div>
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Net revenue</p><p class="sdb-07__kpi-v">$418k</p></div>
</div>
<div class="sdb-07__chart" aria-hidden="true">
<span style="--sdb-07-h:32"></span><span style="--sdb-07-h:48"></span><span style="--sdb-07-h:41"></span>
<span style="--sdb-07-h:66"></span><span style="--sdb-07-h:58"></span><span style="--sdb-07-h:74"></span>
<span style="--sdb-07-h:69"></span><span style="--sdb-07-h:88"></span><span style="--sdb-07-h:79"></span>
</div>
</main>
</div>
</section>.sdb-07 {
--sdb-07-bg: #eef4f1;
--sdb-07-surf: #ffffff;
--sdb-07-surf2: #e2ebe7;
--sdb-07-line: #cbd8d2;
--sdb-07-ink: #16211d;
--sdb-07-mut: #5a6a64;
--sdb-07-acc: #0f7a63;
--sdb-07-acc: oklch(50% 0.1 172);
--sdb-07-acc-soft: #d5eae4;
--sdb-07-w-open: 14.5rem;
--sdb-07-w-rail: 4.25rem;
--sdb-07-font-display: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-07-bg);
color: var(--sdb-07-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-07 *,
.sdb-07 *::before,
.sdb-07 *::after {
box-sizing: border-box;
}
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
@media (prefers-color-scheme: dark) {
.sdb-07 {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #eef4f1;
--sdb-07-surf: #ffffff;
--sdb-07-surf2: #e2ebe7;
--sdb-07-line: #cbd8d2;
--sdb-07-ink: #16211d;
--sdb-07-mut: #5a6a64;
--sdb-07-acc: #0f7a63;
--sdb-07-acc: oklch(50% 0.1 172);
--sdb-07-acc-soft: #d5eae4;
}
}
@media (prefers-color-scheme: light) {
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
}
.sdb-07__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-07__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-07__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 1px solid var(--sdb-07-line);
background: var(--sdb-07-surf);
color: var(--sdb-07-ink);
cursor: pointer;
}
.sdb-07__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-07__theme-input:focus-visible + .sdb-07__theme-label {
outline: 2px solid var(--sdb-07-acc);
outline-offset: 2px;
}
.sdb-07__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-07__app {
display: flex;
min-inline-size: 0;
min-block-size: 100vh;
background: var(--sdb-07-surf);
}
.sdb-07__rail {
flex: none;
inline-size: var(--sdb-07-w-open);
min-inline-size: 0;
display: flex;
flex-direction: column;
border-inline-end: 1px solid var(--sdb-07-line);
background: var(--sdb-07-surf2);
transition: inline-size 200ms ease;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail {
inline-size: var(--sdb-07-w-rail);
}
.sdb-07__app[data-sdb-07-noanim] .sdb-07__rail {
transition: none;
}
.sdb-07__rail-head {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.6rem;
border-block-end: 1px solid var(--sdb-07-line);
}
.sdb-07__mark {
flex: none;
display: grid;
place-items: center;
inline-size: 1.9rem;
block-size: 1.9rem;
background: var(--sdb-07-acc);
color: var(--sdb-07-surf);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
}
.sdb-07__brand {
margin: 0 auto 0 0;
font-family: var(--sdb-07-font-display);
font-size: 0.82rem;
letter-spacing: 0.2em;
text-transform: uppercase;
min-inline-size: 0;
overflow: hidden;
}
.sdb-07__collapse {
flex: none;
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 0;
background: transparent;
color: var(--sdb-07-ink);
cursor: pointer;
}
.sdb-07__collapse svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-07__nav {
padding: 0.4rem 0.35rem;
min-inline-size: 0;
}
.sdb-07__list {
list-style: none;
margin: 0;
padding: 0;
}
.sdb-07__link {
display: flex;
align-items: center;
gap: 0.7rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.6rem;
color: var(--sdb-07-ink);
text-decoration: none;
font-size: 0.9rem;
white-space: nowrap;
min-inline-size: 0;
}
.sdb-07__ico {
flex: none;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.72rem;
color: var(--sdb-07-mut);
inline-size: 1.5rem;
}
.sdb-07__link:hover {
background: var(--sdb-07-surf);
}
.sdb-07__link[aria-current="page"] {
background: var(--sdb-07-acc-soft);
color: var(--sdb-07-acc);
font-weight: 650;
}
.sdb-07__link[aria-current="page"] .sdb-07__ico {
color: var(--sdb-07-acc);
}
.sdb-07__link:focus-visible,
.sdb-07__collapse:focus-visible {
outline: 2px solid var(--sdb-07-acc);
outline-offset: -2px;
}
.sdb-07__hideable {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__hideable {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__brand {
flex: none;
}
/* Collapsed rail is 68px, but the head row needs the 30px mark + 8px gap +
44px button + 19px padding = ~101px, and both children are flex: none, so
the button overflowed 32px past the rail edge. Stack the row instead:
both stay centred, the button keeps its 44px target, and the brand mark
stays visible so the collapsed rail still carries an identity. */
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail-head {
flex-direction: column;
gap: 0.25rem;
padding: 0.4rem 0;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__collapse {
inline-size: 2.75rem;
}
.sdb-07__main {
flex: 1;
min-inline-size: 0;
padding: 1.5rem 1.25rem 2.5rem;
display: grid;
gap: 1.2rem;
align-content: start;
}
@media (min-width: 46rem) {
.sdb-07__main {
padding: 2.4rem 2.25rem 3rem;
}
}
.sdb-07__kpis {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
}
@media (min-width: 34rem) {
.sdb-07__kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.sdb-07__kpi {
min-inline-size: 0;
padding: 0.9rem 1rem;
border: 1px solid var(--sdb-07-line);
}
.sdb-07__kpi-k {
margin: 0 0 0.2rem;
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--sdb-07-mut);
}
.sdb-07__kpi-v {
margin: 0;
font-family: var(--sdb-07-font-display);
font-size: 1.45rem;
font-variant-numeric: tabular-nums;
}
.sdb-07__chart {
display: flex;
align-items: flex-end;
gap: 0.4rem;
block-size: 12rem;
padding: 0.8rem;
border: 1px solid var(--sdb-07-line);
}
.sdb-07__chart span {
flex: 1;
min-inline-size: 0;
block-size: calc(var(--sdb-07-h) * 1%);
background: var(--sdb-07-acc);
opacity: 0.75;
}
@media (max-width: 30rem) {
.sdb-07__app {
flex-direction: column;
}
.sdb-07__rail,
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail {
inline-size: 100%;
border-inline-end: 0;
border-block-end: 1px solid var(--sdb-07-line);
}
}
@media (prefers-reduced-motion: reduce) {
.sdb-07__rail {
transition: none;
}
}
/* The theme toggle is pinned top-right of the demo root, and on narrow
viewports the rail head reaches under it — the toggle then intercepts
every click meant for the collapse button. Reserve the toggle its own
lane. Must sit at the END of the block: the base rail-head rule has
equal specificity and would otherwise win on source order. */
@media (max-width: 30rem) {
.sdb-07__rail-head,
.sdb-07__head {
padding-inline-end: 3.4rem;
}
}.sdb-07 {
--sdb-07-bg: #eef4f1;
--sdb-07-surf: #ffffff;
--sdb-07-surf2: #e2ebe7;
--sdb-07-line: #cbd8d2;
--sdb-07-ink: #16211d;
--sdb-07-mut: #5a6a64;
--sdb-07-acc: #0f7a63;
--sdb-07-acc: oklch(50% 0.1 172);
--sdb-07-acc-soft: #d5eae4;
--sdb-07-w-open: 14.5rem;
--sdb-07-w-rail: 4.25rem;
--sdb-07-font-display: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-07-bg);
color: var(--sdb-07-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-07 *,
.sdb-07 *::before,
.sdb-07 *::after {
box-sizing: border-box;
}
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
@media (prefers-color-scheme: dark) {
.sdb-07 {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #eef4f1;
--sdb-07-surf: #ffffff;
--sdb-07-surf2: #e2ebe7;
--sdb-07-line: #cbd8d2;
--sdb-07-ink: #16211d;
--sdb-07-mut: #5a6a64;
--sdb-07-acc: #0f7a63;
--sdb-07-acc: oklch(50% 0.1 172);
--sdb-07-acc-soft: #d5eae4;
}
}
@media (prefers-color-scheme: light) {
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
}
.sdb-07__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-07__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-07__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 1px solid var(--sdb-07-line);
background: var(--sdb-07-surf);
color: var(--sdb-07-ink);
cursor: pointer;
}
.sdb-07__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-07__theme-input:focus-visible + .sdb-07__theme-label {
outline: 2px solid var(--sdb-07-acc);
outline-offset: 2px;
}
.sdb-07__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-07__app {
display: flex;
min-inline-size: 0;
min-block-size: 100vh;
background: var(--sdb-07-surf);
}
.sdb-07__rail {
flex: none;
inline-size: var(--sdb-07-w-open);
min-inline-size: 0;
display: flex;
flex-direction: column;
border-inline-end: 1px solid var(--sdb-07-line);
background: var(--sdb-07-surf2);
transition: inline-size 200ms ease;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail {
inline-size: var(--sdb-07-w-rail);
}
.sdb-07__app[data-sdb-07-noanim] .sdb-07__rail {
transition: none;
}
.sdb-07__rail-head {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.6rem;
border-block-end: 1px solid var(--sdb-07-line);
}
.sdb-07__mark {
flex: none;
display: grid;
place-items: center;
inline-size: 1.9rem;
block-size: 1.9rem;
background: var(--sdb-07-acc);
color: var(--sdb-07-surf);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
}
.sdb-07__brand {
margin: 0 auto 0 0;
font-family: var(--sdb-07-font-display);
font-size: 0.82rem;
letter-spacing: 0.2em;
text-transform: uppercase;
min-inline-size: 0;
overflow: hidden;
}
.sdb-07__collapse {
flex: none;
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 0;
background: transparent;
color: var(--sdb-07-ink);
cursor: pointer;
}
.sdb-07__collapse svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-07__nav {
padding: 0.4rem 0.35rem;
min-inline-size: 0;
}
.sdb-07__list {
list-style: none;
margin: 0;
padding: 0;
}
.sdb-07__link {
display: flex;
align-items: center;
gap: 0.7rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.6rem;
color: var(--sdb-07-ink);
text-decoration: none;
font-size: 0.9rem;
white-space: nowrap;
min-inline-size: 0;
}
.sdb-07__ico {
flex: none;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.72rem;
color: var(--sdb-07-mut);
inline-size: 1.5rem;
}
.sdb-07__link:hover {
background: var(--sdb-07-surf);
}
.sdb-07__link[aria-current="page"] {
background: var(--sdb-07-acc-soft);
color: var(--sdb-07-acc);
font-weight: 650;
}
.sdb-07__link[aria-current="page"] .sdb-07__ico {
color: var(--sdb-07-acc);
}
.sdb-07__link:focus-visible,
.sdb-07__collapse:focus-visible {
outline: 2px solid var(--sdb-07-acc);
outline-offset: -2px;
}
.sdb-07__hideable {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__hideable {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__brand {
flex: none;
}
/* Collapsed rail is 68px, but the head row needs the 30px mark + 8px gap +
44px button + 19px padding = ~101px, and both children are flex: none, so
the button overflowed 32px past the rail edge. Stack the row instead:
both stay centred, the button keeps its 44px target, and the brand mark
stays visible so the collapsed rail still carries an identity. */
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail-head {
flex-direction: column;
gap: 0.25rem;
padding: 0.4rem 0;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__collapse {
inline-size: 2.75rem;
}
.sdb-07__main {
flex: 1;
min-inline-size: 0;
padding: 1.5rem 1.25rem 2.5rem;
display: grid;
gap: 1.2rem;
align-content: start;
}
@media (min-width: 46rem) {
.sdb-07__main {
padding: 2.4rem 2.25rem 3rem;
}
}
.sdb-07__kpis {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
}
@media (min-width: 34rem) {
.sdb-07__kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.sdb-07__kpi {
min-inline-size: 0;
padding: 0.9rem 1rem;
border: 1px solid var(--sdb-07-line);
}
.sdb-07__kpi-k {
margin: 0 0 0.2rem;
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--sdb-07-mut);
}
.sdb-07__kpi-v {
margin: 0;
font-family: var(--sdb-07-font-display);
font-size: 1.45rem;
font-variant-numeric: tabular-nums;
}
.sdb-07__chart {
display: flex;
align-items: flex-end;
gap: 0.4rem;
block-size: 12rem;
padding: 0.8rem;
border: 1px solid var(--sdb-07-line);
}
.sdb-07__chart span {
flex: 1;
min-inline-size: 0;
block-size: calc(var(--sdb-07-h) * 1%);
background: var(--sdb-07-acc);
opacity: 0.75;
}
@media (max-width: 30rem) {
.sdb-07__app {
flex-direction: column;
}
.sdb-07__rail,
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail {
inline-size: 100%;
border-inline-end: 0;
border-block-end: 1px solid var(--sdb-07-line);
}
}
@media (prefers-reduced-motion: reduce) {
.sdb-07__rail {
transition: none;
}
}
/* The theme toggle is pinned top-right of the demo root, and on narrow
viewports the rail head reaches under it — the toggle then intercepts
every click meant for the collapse button. Reserve the toggle its own
lane. Must sit at the END of the block: the base rail-head rule has
equal specificity and would otherwise win on source order. */
@media (max-width: 30rem) {
.sdb-07__rail-head,
.sdb-07__head {
padding-inline-end: 3.4rem;
}
}const root = document.querySelector('.sdb-07');
const app = root.querySelector('.sdb-07__app');
const btn = root.querySelector('#sdb-07-toggle');
const KEY = 'sdb-07:rail-collapsed';
const read = () => { try { return localStorage.getItem(KEY); } catch (e) { return null; } };
const write = (v) => { try { localStorage.setItem(KEY, v); } catch (e) {} };
function paint(collapsed) {
app.setAttribute('data-sdb-07-collapsed', String(collapsed));
btn.setAttribute('aria-expanded', String(!collapsed));
btn.querySelector('.sdb-07__sr').textContent = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
// In production this runs as an inline script in <head> so the restored width is in the first frame.
app.setAttribute('data-sdb-07-noanim', '');
paint(read() === 'true');
requestAnimationFrame(() => app.removeAttribute('data-sdb-07-noanim'));
function toggle() {
const collapsed = app.getAttribute('data-sdb-07-collapsed') !== 'true';
write(String(collapsed));
paint(collapsed);
}
btn.addEventListener('click', toggle);
root.__sdbCleanup = () => { btn.removeEventListener('click', toggle); };const root = document.querySelector('.sdb-07');
const app = root.querySelector('.sdb-07__app');
const btn = root.querySelector('#sdb-07-toggle');
const KEY = 'sdb-07:rail-collapsed';
const read = () => { try { return localStorage.getItem(KEY); } catch (e) { return null; } };
const write = (v) => { try { localStorage.setItem(KEY, v); } catch (e) {} };
function paint(collapsed) {
app.setAttribute('data-sdb-07-collapsed', String(collapsed));
btn.setAttribute('aria-expanded', String(!collapsed));
btn.querySelector('.sdb-07__sr').textContent = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
// In production this runs as an inline script in <head> so the restored width is in the first frame.
app.setAttribute('data-sdb-07-noanim', '');
paint(read() === 'true');
requestAnimationFrame(() => app.removeAttribute('data-sdb-07-noanim'));
function toggle() {
const collapsed = app.getAttribute('data-sdb-07-collapsed') !== 'true';
write(String(collapsed));
paint(collapsed);
}
btn.addEventListener('click', toggle);
root.__sdbCleanup = () => { btn.removeEventListener('click', toggle); };Here's a working CSS Sidebar Navigation 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: Persisting Sidebar Collapse State Across Reloads
Source: https://codefronts.com/navigation/css-sidebar-navigation/persisting-sidebar-collapse-state-across-reloads/
A sidebar that forgets it was collapsed is a small daily annoyance that compounds. Persist the boolean, then apply it before first paint — an inline script in <head> that sets a class on the root, the same trick a theme toggle uses. Reading the value in a framework effect instead means the wide sidebar paints first and visibly snaps shut. Every read and write is wrapped, because storage throws in sandboxed and private contexts.
## HTML
```html
<section class="sdb-07" aria-label="Prism analytics">
<div class="sdb-07__theme">
<input class="sdb-07__theme-input" type="checkbox" id="sdb-07-theme">
<label class="sdb-07__theme-label" for="sdb-07-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sdb-07__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-07__app" data-sdb-07-collapsed="false" data-sdb-07-noanim>
<aside class="sdb-07__rail" id="sdb-07-rail">
<div class="sdb-07__rail-head">
<span class="sdb-07__mark" aria-hidden="true">PR</span>
<p class="sdb-07__brand"><span class="sdb-07__hideable">Prism</span></p>
<button class="sdb-07__collapse" type="button" id="sdb-07-toggle" aria-expanded="true" aria-controls="sdb-07-rail">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h10M4 18h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span class="sdb-07__sr">Collapse sidebar</span>
</button>
</div>
<nav class="sdb-07__nav" aria-label="Prism reports">
<ul class="sdb-07__list">
<li><a class="sdb-07__link" href="#sdb-07-main" aria-current="page"><span class="sdb-07__ico" aria-hidden="true">01</span><span class="sdb-07__hideable">Overview</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">02</span><span class="sdb-07__hideable">Acquisition</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">03</span><span class="sdb-07__hideable">Retention</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">04</span><span class="sdb-07__hideable">Revenue</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">05</span><span class="sdb-07__hideable">Audit log</span></a></li>
</ul>
</nav>
</aside>
<main class="sdb-07__main" id="sdb-07-main">
<div class="sdb-07__kpis">
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Active workspaces</p><p class="sdb-07__kpi-v">1,284</p></div>
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Weekly retention</p><p class="sdb-07__kpi-v">71.4%</p></div>
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Net revenue</p><p class="sdb-07__kpi-v">$418k</p></div>
</div>
<div class="sdb-07__chart" aria-hidden="true">
<span style="--sdb-07-h:32"></span><span style="--sdb-07-h:48"></span><span style="--sdb-07-h:41"></span>
<span style="--sdb-07-h:66"></span><span style="--sdb-07-h:58"></span><span style="--sdb-07-h:74"></span>
<span style="--sdb-07-h:69"></span><span style="--sdb-07-h:88"></span><span style="--sdb-07-h:79"></span>
</div>
</main>
</div>
</section>
```
## CSS
```css
.sdb-07 {
--sdb-07-bg: #eef4f1;
--sdb-07-surf: #ffffff;
--sdb-07-surf2: #e2ebe7;
--sdb-07-line: #cbd8d2;
--sdb-07-ink: #16211d;
--sdb-07-mut: #5a6a64;
--sdb-07-acc: #0f7a63;
--sdb-07-acc: oklch(50% 0.1 172);
--sdb-07-acc-soft: #d5eae4;
--sdb-07-w-open: 14.5rem;
--sdb-07-w-rail: 4.25rem;
--sdb-07-font-display: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-07-bg);
color: var(--sdb-07-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-07 *,
.sdb-07 *::before,
.sdb-07 *::after {
box-sizing: border-box;
}
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
@media (prefers-color-scheme: dark) {
.sdb-07 {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #eef4f1;
--sdb-07-surf: #ffffff;
--sdb-07-surf2: #e2ebe7;
--sdb-07-line: #cbd8d2;
--sdb-07-ink: #16211d;
--sdb-07-mut: #5a6a64;
--sdb-07-acc: #0f7a63;
--sdb-07-acc: oklch(50% 0.1 172);
--sdb-07-acc-soft: #d5eae4;
}
}
@media (prefers-color-scheme: light) {
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
}
.sdb-07__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-07__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-07__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 1px solid var(--sdb-07-line);
background: var(--sdb-07-surf);
color: var(--sdb-07-ink);
cursor: pointer;
}
.sdb-07__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-07__theme-input:focus-visible + .sdb-07__theme-label {
outline: 2px solid var(--sdb-07-acc);
outline-offset: 2px;
}
.sdb-07__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-07__app {
display: flex;
min-inline-size: 0;
min-block-size: 100vh;
background: var(--sdb-07-surf);
}
.sdb-07__rail {
flex: none;
inline-size: var(--sdb-07-w-open);
min-inline-size: 0;
display: flex;
flex-direction: column;
border-inline-end: 1px solid var(--sdb-07-line);
background: var(--sdb-07-surf2);
transition: inline-size 200ms ease;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail {
inline-size: var(--sdb-07-w-rail);
}
.sdb-07__app[data-sdb-07-noanim] .sdb-07__rail {
transition: none;
}
.sdb-07__rail-head {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.6rem;
border-block-end: 1px solid var(--sdb-07-line);
}
.sdb-07__mark {
flex: none;
display: grid;
place-items: center;
inline-size: 1.9rem;
block-size: 1.9rem;
background: var(--sdb-07-acc);
color: var(--sdb-07-surf);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
}
.sdb-07__brand {
margin: 0 auto 0 0;
font-family: var(--sdb-07-font-display);
font-size: 0.82rem;
letter-spacing: 0.2em;
text-transform: uppercase;
min-inline-size: 0;
overflow: hidden;
}
.sdb-07__collapse {
flex: none;
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 0;
background: transparent;
color: var(--sdb-07-ink);
cursor: pointer;
}
.sdb-07__collapse svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-07__nav {
padding: 0.4rem 0.35rem;
min-inline-size: 0;
}
.sdb-07__list {
list-style: none;
margin: 0;
padding: 0;
}
.sdb-07__link {
display: flex;
align-items: center;
gap: 0.7rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.6rem;
color: var(--sdb-07-ink);
text-decoration: none;
font-size: 0.9rem;
white-space: nowrap;
min-inline-size: 0;
}
.sdb-07__ico {
flex: none;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.72rem;
color: var(--sdb-07-mut);
inline-size: 1.5rem;
}
.sdb-07__link:hover {
background: var(--sdb-07-surf);
}
.sdb-07__link[aria-current="page"] {
background: var(--sdb-07-acc-soft);
color: var(--sdb-07-acc);
font-weight: 650;
}
.sdb-07__link[aria-current="page"] .sdb-07__ico {
color: var(--sdb-07-acc);
}
.sdb-07__link:focus-visible,
.sdb-07__collapse:focus-visible {
outline: 2px solid var(--sdb-07-acc);
outline-offset: -2px;
}
.sdb-07__hideable {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__hideable {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__brand {
flex: none;
}
/* Collapsed rail is 68px, but the head row needs the 30px mark + 8px gap +
44px button + 19px padding = ~101px, and both children are flex: none, so
the button overflowed 32px past the rail edge. Stack the row instead:
both stay centred, the button keeps its 44px target, and the brand mark
stays visible so the collapsed rail still carries an identity. */
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail-head {
flex-direction: column;
gap: 0.25rem;
padding: 0.4rem 0;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__collapse {
inline-size: 2.75rem;
}
.sdb-07__main {
flex: 1;
min-inline-size: 0;
padding: 1.5rem 1.25rem 2.5rem;
display: grid;
gap: 1.2rem;
align-content: start;
}
@media (min-width: 46rem) {
.sdb-07__main {
padding: 2.4rem 2.25rem 3rem;
}
}
.sdb-07__kpis {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
}
@media (min-width: 34rem) {
.sdb-07__kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.sdb-07__kpi {
min-inline-size: 0;
padding: 0.9rem 1rem;
border: 1px solid var(--sdb-07-line);
}
.sdb-07__kpi-k {
margin: 0 0 0.2rem;
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--sdb-07-mut);
}
.sdb-07__kpi-v {
margin: 0;
font-family: var(--sdb-07-font-display);
font-size: 1.45rem;
font-variant-numeric: tabular-nums;
}
.sdb-07__chart {
display: flex;
align-items: flex-end;
gap: 0.4rem;
block-size: 12rem;
padding: 0.8rem;
border: 1px solid var(--sdb-07-line);
}
.sdb-07__chart span {
flex: 1;
min-inline-size: 0;
block-size: calc(var(--sdb-07-h) * 1%);
background: var(--sdb-07-acc);
opacity: 0.75;
}
@media (max-width: 30rem) {
.sdb-07__app {
flex-direction: column;
}
.sdb-07__rail,
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail {
inline-size: 100%;
border-inline-end: 0;
border-block-end: 1px solid var(--sdb-07-line);
}
}
@media (prefers-reduced-motion: reduce) {
.sdb-07__rail {
transition: none;
}
}
/* The theme toggle is pinned top-right of the demo root, and on narrow
viewports the rail head reaches under it — the toggle then intercepts
every click meant for the collapse button. Reserve the toggle its own
lane. Must sit at the END of the block: the base rail-head rule has
equal specificity and would otherwise win on source order. */
@media (max-width: 30rem) {
.sdb-07__rail-head,
.sdb-07__head {
padding-inline-end: 3.4rem;
}
}
```
## JavaScript
```js
const root = document.querySelector('.sdb-07');
const app = root.querySelector('.sdb-07__app');
const btn = root.querySelector('#sdb-07-toggle');
const KEY = 'sdb-07:rail-collapsed';
const read = () => { try { return localStorage.getItem(KEY); } catch (e) { return null; } };
const write = (v) => { try { localStorage.setItem(KEY, v); } catch (e) {} };
function paint(collapsed) {
app.setAttribute('data-sdb-07-collapsed', String(collapsed));
btn.setAttribute('aria-expanded', String(!collapsed));
btn.querySelector('.sdb-07__sr').textContent = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
// In production this runs as an inline script in <head> so the restored width is in the first frame.
app.setAttribute('data-sdb-07-noanim', '');
paint(read() === 'true');
requestAnimationFrame(() => app.removeAttribute('data-sdb-07-noanim'));
function toggle() {
const collapsed = app.getAttribute('data-sdb-07-collapsed') !== 'true';
write(String(collapsed));
paint(collapsed);
}
btn.addEventListener('click', toggle);
root.__sdbCleanup = () => { btn.removeEventListener('click', toggle); };
```Here's a working CSS Sidebar Navigation 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: Persisting Sidebar Collapse State Across Reloads
Source: https://codefronts.com/navigation/css-sidebar-navigation/persisting-sidebar-collapse-state-across-reloads/
A sidebar that forgets it was collapsed is a small daily annoyance that compounds. Persist the boolean, then apply it before first paint — an inline script in <head> that sets a class on the root, the same trick a theme toggle uses. Reading the value in a framework effect instead means the wide sidebar paints first and visibly snaps shut. Every read and write is wrapped, because storage throws in sandboxed and private contexts.
## HTML
```html
<section class="sdb-07" aria-label="Prism analytics">
<div class="sdb-07__theme">
<input class="sdb-07__theme-input" type="checkbox" id="sdb-07-theme">
<label class="sdb-07__theme-label" for="sdb-07-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sdb-07__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-07__app" data-sdb-07-collapsed="false" data-sdb-07-noanim>
<aside class="sdb-07__rail" id="sdb-07-rail">
<div class="sdb-07__rail-head">
<span class="sdb-07__mark" aria-hidden="true">PR</span>
<p class="sdb-07__brand"><span class="sdb-07__hideable">Prism</span></p>
<button class="sdb-07__collapse" type="button" id="sdb-07-toggle" aria-expanded="true" aria-controls="sdb-07-rail">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h10M4 18h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span class="sdb-07__sr">Collapse sidebar</span>
</button>
</div>
<nav class="sdb-07__nav" aria-label="Prism reports">
<ul class="sdb-07__list">
<li><a class="sdb-07__link" href="#sdb-07-main" aria-current="page"><span class="sdb-07__ico" aria-hidden="true">01</span><span class="sdb-07__hideable">Overview</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">02</span><span class="sdb-07__hideable">Acquisition</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">03</span><span class="sdb-07__hideable">Retention</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">04</span><span class="sdb-07__hideable">Revenue</span></a></li>
<li><a class="sdb-07__link" href="#sdb-07-main"><span class="sdb-07__ico" aria-hidden="true">05</span><span class="sdb-07__hideable">Audit log</span></a></li>
</ul>
</nav>
</aside>
<main class="sdb-07__main" id="sdb-07-main">
<div class="sdb-07__kpis">
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Active workspaces</p><p class="sdb-07__kpi-v">1,284</p></div>
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Weekly retention</p><p class="sdb-07__kpi-v">71.4%</p></div>
<div class="sdb-07__kpi"><p class="sdb-07__kpi-k">Net revenue</p><p class="sdb-07__kpi-v">$418k</p></div>
</div>
<div class="sdb-07__chart" aria-hidden="true">
<span style="--sdb-07-h:32"></span><span style="--sdb-07-h:48"></span><span style="--sdb-07-h:41"></span>
<span style="--sdb-07-h:66"></span><span style="--sdb-07-h:58"></span><span style="--sdb-07-h:74"></span>
<span style="--sdb-07-h:69"></span><span style="--sdb-07-h:88"></span><span style="--sdb-07-h:79"></span>
</div>
</main>
</div>
</section>
```
## CSS
```css
.sdb-07 {
--sdb-07-bg: #eef4f1;
--sdb-07-surf: #ffffff;
--sdb-07-surf2: #e2ebe7;
--sdb-07-line: #cbd8d2;
--sdb-07-ink: #16211d;
--sdb-07-mut: #5a6a64;
--sdb-07-acc: #0f7a63;
--sdb-07-acc: oklch(50% 0.1 172);
--sdb-07-acc-soft: #d5eae4;
--sdb-07-w-open: 14.5rem;
--sdb-07-w-rail: 4.25rem;
--sdb-07-font-display: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-07-bg);
color: var(--sdb-07-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-07 *,
.sdb-07 *::before,
.sdb-07 *::after {
box-sizing: border-box;
}
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
@media (prefers-color-scheme: dark) {
.sdb-07 {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #eef4f1;
--sdb-07-surf: #ffffff;
--sdb-07-surf2: #e2ebe7;
--sdb-07-line: #cbd8d2;
--sdb-07-ink: #16211d;
--sdb-07-mut: #5a6a64;
--sdb-07-acc: #0f7a63;
--sdb-07-acc: oklch(50% 0.1 172);
--sdb-07-acc-soft: #d5eae4;
}
}
@media (prefers-color-scheme: light) {
.sdb-07:has(.sdb-07__theme-input:checked) {
--sdb-07-bg: #0b1210;
--sdb-07-surf: #121a18;
--sdb-07-surf2: #18221f;
--sdb-07-line: #26332f;
--sdb-07-ink: #e9f1ee;
--sdb-07-mut: #93a4a0;
--sdb-07-acc: #62d9b8;
--sdb-07-acc: oklch(82% 0.12 172);
--sdb-07-acc-soft: #12302a;
}
}
.sdb-07__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-07__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-07__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 1px solid var(--sdb-07-line);
background: var(--sdb-07-surf);
color: var(--sdb-07-ink);
cursor: pointer;
}
.sdb-07__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-07__theme-input:focus-visible + .sdb-07__theme-label {
outline: 2px solid var(--sdb-07-acc);
outline-offset: 2px;
}
.sdb-07__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-07__app {
display: flex;
min-inline-size: 0;
min-block-size: 100vh;
background: var(--sdb-07-surf);
}
.sdb-07__rail {
flex: none;
inline-size: var(--sdb-07-w-open);
min-inline-size: 0;
display: flex;
flex-direction: column;
border-inline-end: 1px solid var(--sdb-07-line);
background: var(--sdb-07-surf2);
transition: inline-size 200ms ease;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail {
inline-size: var(--sdb-07-w-rail);
}
.sdb-07__app[data-sdb-07-noanim] .sdb-07__rail {
transition: none;
}
.sdb-07__rail-head {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.6rem;
border-block-end: 1px solid var(--sdb-07-line);
}
.sdb-07__mark {
flex: none;
display: grid;
place-items: center;
inline-size: 1.9rem;
block-size: 1.9rem;
background: var(--sdb-07-acc);
color: var(--sdb-07-surf);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
}
.sdb-07__brand {
margin: 0 auto 0 0;
font-family: var(--sdb-07-font-display);
font-size: 0.82rem;
letter-spacing: 0.2em;
text-transform: uppercase;
min-inline-size: 0;
overflow: hidden;
}
.sdb-07__collapse {
flex: none;
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 0;
background: transparent;
color: var(--sdb-07-ink);
cursor: pointer;
}
.sdb-07__collapse svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-07__nav {
padding: 0.4rem 0.35rem;
min-inline-size: 0;
}
.sdb-07__list {
list-style: none;
margin: 0;
padding: 0;
}
.sdb-07__link {
display: flex;
align-items: center;
gap: 0.7rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.6rem;
color: var(--sdb-07-ink);
text-decoration: none;
font-size: 0.9rem;
white-space: nowrap;
min-inline-size: 0;
}
.sdb-07__ico {
flex: none;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.72rem;
color: var(--sdb-07-mut);
inline-size: 1.5rem;
}
.sdb-07__link:hover {
background: var(--sdb-07-surf);
}
.sdb-07__link[aria-current="page"] {
background: var(--sdb-07-acc-soft);
color: var(--sdb-07-acc);
font-weight: 650;
}
.sdb-07__link[aria-current="page"] .sdb-07__ico {
color: var(--sdb-07-acc);
}
.sdb-07__link:focus-visible,
.sdb-07__collapse:focus-visible {
outline: 2px solid var(--sdb-07-acc);
outline-offset: -2px;
}
.sdb-07__hideable {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__hideable {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__brand {
flex: none;
}
/* Collapsed rail is 68px, but the head row needs the 30px mark + 8px gap +
44px button + 19px padding = ~101px, and both children are flex: none, so
the button overflowed 32px past the rail edge. Stack the row instead:
both stay centred, the button keeps its 44px target, and the brand mark
stays visible so the collapsed rail still carries an identity. */
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail-head {
flex-direction: column;
gap: 0.25rem;
padding: 0.4rem 0;
}
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__collapse {
inline-size: 2.75rem;
}
.sdb-07__main {
flex: 1;
min-inline-size: 0;
padding: 1.5rem 1.25rem 2.5rem;
display: grid;
gap: 1.2rem;
align-content: start;
}
@media (min-width: 46rem) {
.sdb-07__main {
padding: 2.4rem 2.25rem 3rem;
}
}
.sdb-07__kpis {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
}
@media (min-width: 34rem) {
.sdb-07__kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.sdb-07__kpi {
min-inline-size: 0;
padding: 0.9rem 1rem;
border: 1px solid var(--sdb-07-line);
}
.sdb-07__kpi-k {
margin: 0 0 0.2rem;
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--sdb-07-mut);
}
.sdb-07__kpi-v {
margin: 0;
font-family: var(--sdb-07-font-display);
font-size: 1.45rem;
font-variant-numeric: tabular-nums;
}
.sdb-07__chart {
display: flex;
align-items: flex-end;
gap: 0.4rem;
block-size: 12rem;
padding: 0.8rem;
border: 1px solid var(--sdb-07-line);
}
.sdb-07__chart span {
flex: 1;
min-inline-size: 0;
block-size: calc(var(--sdb-07-h) * 1%);
background: var(--sdb-07-acc);
opacity: 0.75;
}
@media (max-width: 30rem) {
.sdb-07__app {
flex-direction: column;
}
.sdb-07__rail,
.sdb-07__app[data-sdb-07-collapsed="true"] .sdb-07__rail {
inline-size: 100%;
border-inline-end: 0;
border-block-end: 1px solid var(--sdb-07-line);
}
}
@media (prefers-reduced-motion: reduce) {
.sdb-07__rail {
transition: none;
}
}
/* The theme toggle is pinned top-right of the demo root, and on narrow
viewports the rail head reaches under it — the toggle then intercepts
every click meant for the collapse button. Reserve the toggle its own
lane. Must sit at the END of the block: the base rail-head rule has
equal specificity and would otherwise win on source order. */
@media (max-width: 30rem) {
.sdb-07__rail-head,
.sdb-07__head {
padding-inline-end: 3.4rem;
}
}
```
## JavaScript
```js
const root = document.querySelector('.sdb-07');
const app = root.querySelector('.sdb-07__app');
const btn = root.querySelector('#sdb-07-toggle');
const KEY = 'sdb-07:rail-collapsed';
const read = () => { try { return localStorage.getItem(KEY); } catch (e) { return null; } };
const write = (v) => { try { localStorage.setItem(KEY, v); } catch (e) {} };
function paint(collapsed) {
app.setAttribute('data-sdb-07-collapsed', String(collapsed));
btn.setAttribute('aria-expanded', String(!collapsed));
btn.querySelector('.sdb-07__sr').textContent = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
// In production this runs as an inline script in <head> so the restored width is in the first frame.
app.setAttribute('data-sdb-07-noanim', '');
paint(read() === 'true');
requestAnimationFrame(() => app.removeAttribute('data-sdb-07-noanim'));
function toggle() {
const collapsed = app.getAttribute('data-sdb-07-collapsed') !== 'true';
write(String(collapsed));
paint(collapsed);
}
btn.addEventListener('click', toggle);
root.__sdbCleanup = () => { btn.removeEventListener('click', toggle); };
```How this works
Persisting the value is trivial; applying it early is the whole problem. Writing collapsed to localStorage on toggle is one line. The bug appears on the next load: the markup renders at its default width, then whatever code reads storage runs, then the class changes — and the user watches the sidebar snap from 260px to 68px. In a framework this is worse, because reading storage in an effect runs after hydration, which is at least one full paint too late.
The fix is a synchronous script before the body renders. An inline <script> in <head> reads the key and sets a class or data attribute on the root element. It is blocking, it is tiny, and it runs before the first paint, so there is no wrong frame to see. This is exactly the pattern used to avoid the flash of the wrong theme, and it is the same shape for any state that affects layout above the fold.
Storage is not guaranteed to work, and the failure is a thrown exception rather than a null. In a sandboxed iframe, in Safari private browsing under some settings, and when a user has blocked site data, touching localStorage throws a SecurityError. An unguarded read at the top of an inline script therefore takes the whole script down with it, which means whatever else that script was doing — often the theme — also fails. Wrap both directions and treat failure as "no stored preference".
The second trap is animating the restored state. If the collapse transition is live during the first paint, restoring "collapsed" animates the rail closed on every load, which looks like a bug even though the state is correct. Suppress transitions for the first frame — here a data attribute removed on the next animation frame — and the restored state simply is, rather than becoming.
Make it yours
- Rename the storage key to something namespaced to your app; a bare
sidebarkey will collide with any other script on the same origin. - Store an enum instead of a boolean if you have more than two widths — expanded, rail, hidden — and switch on it in the same pre-paint script.
- Move the persistence to a cookie if the state must be known server-side, so the markup can be rendered collapsed and there is nothing to correct on the client.
- Scope the key per user or per workspace by including an id, so switching accounts does not inherit the previous layout.
- Add a session-only mode by swapping
localStorageforsessionStorage; the guard pattern is identical. - Move the restore into an inline
<script>in<head>in your own page — the demo runs it as early as a snippet can.
Gotchas — read before shipping
- Reading the value in a
useEffector onDOMContentLoadedis always at least one paint too late. The sidebar renders wide, then jumps — and it happens on every single load. - An unguarded
localStorageread throws in sandboxed iframes and some private-browsing modes, and it takes the rest of that inline script with it. Guard both the read and the write. - Leaving the width transition active during the first paint animates the restored state on every load, which reads as the sidebar closing itself.
- Persisting the collapsed state without also restoring
aria-expandedleaves the control announcing the opposite of what is on screen — correct visual, wrong announcement.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 121+ | 111+ |
localStorage is Baseline since 2009, so oklch() and the :has() theme toggle set the floor. The real compatibility risk is not the version but the context: sandboxed iframes and blocked-storage modes throw on access, which is why every read and write here is wrapped and the demo renders correctly with no stored value at all.