Dashboard Sidebar Design + 9 Widget UI Pieces
Here, a dashboard sidebar design means just this one piece, not the whole admin page — plus eight more standalone dashboard widgets, including a donut chart
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Standalone sidebar nav
- 02 Stat card row with sparkline reveal
- 03 Sortable mini table
- 04 Donut chart card with center counter
- 05 Chip filter bar
- 06 Empty state with add-morph
- 07 Settings form section with save state
- 08 Notifications dropdown panel
- 09 User avatar dropdown menu
These nine are ordered by where your eye lands on a real screen, not by popularity. First the structural piece (sidebar), then the at-a-glance summaries (stats, table, chart), then the pieces a visitor actually touches (filters, empty state, settings), and last the two small popovers tucked in a header corner (notifications, user menu). Every demo drops just that one widget in the middle of the frame — no surrounding admin chrome — so you can lift the exact markup into whatever page you already have.
01Standalone sidebar nav
Click the collapse button and the sidebar width actually shrinks from 208px to 76px while the text labels fade out and only the icons stay. The current page is marked with a thin left bar and a soft background tint.
.sd { width: 208px; transition: width $duration $easing; }
.sd.is-collapsed { width: 76px; }
.sd__label { transition: opacity $duration $easing; }
.sd.is-collapsed .sd__label { opacity: 0; }
02Stat card row with sparkline reveal
Switch the period toggle and all three numbers actually swap. Each mini chart line underneath uses an SVG pathLength of 100, so the same dash-offset math draws any shape the same way.
.sc__spark-path { stroke-dasharray: 100; stroke-dashoffset: 100; }
.sc__card.is-demo .sc__spark-path { animation: sc-draw 2s $easing infinite; }
@keyframes sc-draw {
0% { stroke-dashoffset: 100; }
60% { stroke-dashoffset: 0; }
}
03Sortable mini table
Click a column header and the rows are actually re-sorted in the DOM, not just visually reordered with CSS. The amount column compares numbers; the name column compares strings.
rows.sort(function (a, b) {
var av = a.dataset[key], bv = b.dataset[key];
if (key === 'amount') { av = Number(av); bv = Number(bv); return (av - bv) * dir; }
return av.localeCompare(bv) * dir;
});
rows.forEach(function (r) { tbody.appendChild(r); });
04Donut chart card with center counter
Toggle a legend item and that slice actually drops out of the ring while the center number counts to the new total. The ring itself is plain conic-gradient, no SVG at all.
@property --pct-a { syntax: "<percentage>"; inherits: true; initial-value: 0%; }
.cc__ring {
background: conic-gradient($color 0 var(--pct-a), $stage-yellow var(--pct-a) var(--pct-b), rgba(23,20,26,.08) var(--pct-b) 100%);
mask: radial-gradient(farthest-side, transparent 60%, #000 61%);
transition: --pct-a $duration $easing, --pct-b $duration $easing;
}
05Chip filter bar
Every chip you click actually flips to a pressed state (filled pill, aria-pressed="true") and the selected count updates. A "clear all" text button only shows up once something is selected.
c.addEventListener('click', function () {
c.classList.remove('is-demo');
var on = c.getAttribute('aria-pressed') === 'true';
c.setAttribute('aria-pressed', String(!on));
refresh();
});
06Empty state with add-morph
A shape-only illustration floats gently until you click "add first item" — then that same spot actually morphs into one real list card. Click again and it morphs back.
.es__empty, .es__item { position: absolute; inset: 0; transition: opacity $duration $easing, transform $duration $easing; }
.es__item { opacity: 0; transform: scale(.9); }
.es.is-added .es__item { opacity: 1; transform: scale(1); }
.es.is-added .es__empty { opacity: 0; transform: scale(.9); }
07Settings form section with save state
Flip a toggle, hit save, and the button actually cycles through three states — a spinning ring, then a green "saved" state 700ms later, reverting after 1.6s. It's one closed <form>, meant to be lifted as a section, not a whole settings page.
save.classList.add('is-busy'); label.textContent = 'Saving';
setTimeout(function () {
save.classList.remove('is-busy'); save.classList.add('is-done'); label.textContent = 'Saved';
setTimeout(function () { save.classList.remove('is-done'); label.textContent = 'Save'; }, 1600);
}, 700);
08Notifications dropdown panel
Click the bell and the panel actually grows open from that corner. Click one notification and only that row is marked read while the badge count drops by exactly one.
function syncBadge() {
var n = document.querySelectorAll('.np__item.is-unread').length;
badge.textContent = String(n);
badge.style.display = n > 0 ? '' : 'none';
}
it.addEventListener('click', function () { it.classList.remove('is-unread'); syncBadge(); });
09User avatar dropdown menu
Click the avatar and the menu actually opens anchored to the top-right corner. Click anywhere outside, or press Escape, and it actually closes — clicking inside the menu itself does not.
document.addEventListener('click', function (e) {
if (!e.target.closest('.um')) close();
});
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
Where it breaks — the trap
The real trap across these nine was vertical position on the two popovers (notifications, user menu). The stage centers content in a 480×300 frame, so the bell or avatar landed dead center — and the panel opening downward from there ran straight past the bottom edge, forcing a scrollbar. Anchoring the stage to the top instead of the center was the actual fix, and at a narrow 320×200 phone frame it still wasn't enough — the panel title and description lines had to be hidden there too, leaving only the bold item names. The sortable table's first version highlighted each row's background in sequence, but measuring it showed almost no frame-to-frame change — it read as "not moving" even though something clearly cycled on screen. Swapping that for one highlight band that actually translates down the table fixed it — open the zip with password rzae7dqh and check the current build, and the table now moves as clearly as the other eight. The donut chart started as two SVG arcs, but their start angles kept drifting out of sync by a few degrees; switching to conic-gradient plus a registered @property made a single value change animate cleanly instead.
Accessibility
All nine turn off their looping decorative motion under prefers-reduced-motion: reduce — the sidebar's auto-collapse loop, the sparkline auto-draw, the filter chips auto-press, the bell and avatar pulse — while keeping the real state as-is. The notifications panel (08) and user menu (09) both use role="menu"/role="menuitem" and close on an outside click; only the user menu also closes on Escape. The settings toggle (07) exposes role="switch" with aria-checked and responds only to Enter and Space. The sortable table (03) announces its current order through aria-sort, and the empty state (06) actually toggles aria-hidden on the swapped-in card so a screen reader doesn't read a card that isn't visible. This follows MDN's ARIA widget roles reference.
More dashboard-adjacent pieces live in the template category, and what this site does overall is on the about page.
FAQ
Can I use just one of these widgets instead of a full dashboard template?
Yes — that's the whole point of this set. Each demo already shows one widget alone on the frame, not a full page, and the zip ships that same single widget's HTML, SCSS, and JS so you can move it straight into your own sidebar or header.
Does the React version behave differently from the CSS version?
The behavior is the same. Open/closed and selection state live in useState, and the three variables (duration, easing, color) come in as props mapped to CSS custom properties. The donut counter's count-up runs the same math inside useEffect with requestAnimationFrame.
Is the notification badge or sidebar collapse actually functional, or just decoration?
They're functional. Clicking a notification really flips its unread state and the badge recount reflects what's left; the sidebar toggle really changes the width value. The looping motion you see in the auto-playing GIF is a decorative loop that only runs before anyone interacts — it stops the instant you click or tap.