9 Dashboard Components, Not the Whole Page
Dashboard components are UI pieces — a header, a feed, a calendar — you drop in one at a time instead of wiring a whole admin screen.
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Breadcrumb + page header
- 02 Activity feed timeline
- 03 Monthly calendar widget
- 04 Mini kanban board widget
- 05 Goal progress step card
- 06 Team member list with search
- 07 Command palette (⌘K)
- 08 Tag manager input widget
- 09 File upload dropzone
The order here isn't popularity — it's the order a person actually moves through a dashboard. It starts with the header that tells you where you are, then recent activity, then planning (calendar, kanban), then a goal you're tracking, then people and search, and it ends with cleanup (tags, files). Each demo keeps a single widget centered on its own screen so you can lift out one piece instead of an entire admin template.
01Breadcrumb + page header
Clicking the kebab button actually scales a menu open from the top-right corner, and clicking outside or pressing Escape actually closes it. Good for a page that just needs a path indicator and an action menu, not a full admin shell.
function close() {
wrap.classList.remove('is-open');
kebab.setAttribute('aria-expanded', 'false');
}
kebab.addEventListener('click', function (e) {
document.querySelector('.ph').classList.remove('is-demo');
e.stopPropagation();
var open = wrap.classList.toggle('is-open');
kebab.setAttribute('aria-expanded', String(open));
});
document.addEventListener('click', function (e) {
if (!e.target.closest('.ph__menu-wrap')) close();
});
02Activity feed timeline
Clicking "show new activity" actually slides a hidden entry up into view, and each type gets its own colored icon for completed, comment, or upload.
.af__item--new {
opacity: 0;
transform: translateY(10px);
transition: opacity $duration $easing, transform $duration $easing;
}
.af__item--new.af__item--shown { opacity: 1; transform: translateY(0); }
.af__item--new.is-demo { animation: af-in 2s $easing infinite; }
@keyframes af-in {
0% { opacity: 0; transform: translateY(10px); }
35% { opacity: 1; transform: translateY(0); }
80% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(10px); }
}
03Monthly calendar widget
Clicking an arrow actually advances the month and rebuilds that month's grid with a native Date object, marking event days with a dot and today with a ring.
document.querySelectorAll('.cal__nav').forEach(function (btn) {
btn.addEventListener('click', function () {
cur.m += Number(btn.dataset.dir);
if (cur.m < 0) { cur.m = 11; cur.y--; }
if (cur.m > 11) { cur.m = 0; cur.y++; }
build();
});
});
04Mini kanban board widget
Clicking a card's arrow button actually moves that card into the next column's DOM node, and once it lands in the last column the arrow becomes a check mark instead. No drag library, just three progress columns.
@keyframes kb-move {
0% { transform: translateX(0) translateY(0); opacity: .9; }
45% { transform: translateX(96px) translateY(-3px); opacity: .9; }
90% { transform: translateX(192px) translateY(0); opacity: .9; }
96% { transform: translateX(192px) translateY(0); opacity: 0; }
100% { transform: translateX(0) translateY(0); opacity: 0; }
}
05Goal progress step card
Clicking "next step" actually fills the bar to that step's value and stamps a check on the step just passed, and the button disappears once the last step is reached.
function apply() {
steps.forEach(function (s, i) { s.classList.toggle('is-active', i <= idx); });
fill.style.transform = 'scaleX(' + (Number(steps[idx].dataset.p) / 100) + ')';
pct.textContent = (idx + 1) + ' / ' + steps.length;
btn.hidden = idx === steps.length - 1;
}
btn.addEventListener('click', function () {
document.querySelector('.gt').classList.remove('is-demo');
idx = Math.min(idx + 1, steps.length - 1);
apply();
});
06Team member list with search
Typing a name actually keeps only matching rows visible and hides the rest, while the online status dot keeps emitting a faint expanding ring.
input.addEventListener('input', function () {
var q = input.value.trim().toLowerCase();
var shown = 0;
rows.forEach(function (r) {
var hit = r.dataset.name.toLowerCase().indexOf(q) !== -1;
r.hidden = !hit;
if (hit) shown++;
});
empty.hidden = shown !== 0;
});
07Command palette (Ctrl+K)
Clicking the trigger or pressing Ctrl (or Cmd) + K actually scales the panel open at the center of the screen, typing actually filters the command list, and the arrow keys plus Enter pick one.
function filter() {
var q = input.value.trim().toLowerCase();
items.forEach(function (it) { it.hidden = q !== '' && it.dataset.kw.toLowerCase().indexOf(q) === -1; });
var vis = visibleItems();
empty.hidden = vis.length !== 0;
setActive(0, vis);
}
08Tag manager input widget
Typing a tag and clicking add (or pressing Enter) actually pushes a new chip onto the end of the list, and clicking a chip's × actually removes just that one tag.
function addTag() {
var v = input.value.trim();
if (!v) return;
var li = document.createElement('li');
li.className = 'tg__chip';
li.dataset.tag = v;
li.textContent = v;
var rm = document.createElement('button');
rm.type = 'button'; rm.className = 'tg__remove'; rm.textContent = '×';
rm.setAttribute('aria-label', v + ' remove');
li.appendChild(rm);
wireRemove(li);
list.appendChild(li);
input.value = '';
input.focus({ preventScroll: true });
}
09File upload dropzone
Dropping a file or picking one from the file browser actually fills a progress bar from zero to full, then swaps it for a check icon once it's done.
function addFile(name) {
var li = document.createElement('li');
li.className = 'fu__file';
var nameEl = document.createElement('span'); nameEl.className = 'fu__file-name'; nameEl.textContent = name;
var track = document.createElement('span'); track.className = 'fu__track';
var fill = document.createElement('span'); fill.className = 'fu__fill';
track.appendChild(fill);
var rm = document.createElement('button'); rm.type = 'button'; rm.className = 'fu__remove';
rm.setAttribute('aria-label', name + ' remove'); rm.textContent = '×';
rm.addEventListener('click', function () { li.remove(); });
li.appendChild(nameEl); li.appendChild(track); li.appendChild(rm);
list.appendChild(li);
requestAnimationFrame(function () { fill.style.transform = 'scaleX(1)'; });
setTimeout(function () { li.classList.add('fu__file--done'); }, 900);
}
Where it breaks — the trap
The real trap while building these nine was an element that expands past its own box, like the kebab menu. It sat fine at the 480×300 desktop size, but re-measuring at a 320×200 phone viewport showed the dropdown turning half the screen into scrollable space — overflow: hidden hides the overflow visually, but scrollHeight still measures the full laid-out size underneath it, so the fix was trimming the menu's item count and repositioning it until the phone check actually passed. The kanban board's ghost card hit a related trap: teleporting it between three columns left the animation frozen for most of the capture window, which the measurement script read as "not moving" — switching to one continuous two-second slide instead of three discrete jumps is what made all nine register as actually moving. There was a smaller trap in the translation dictionary too — the kanban board (04) and the goal card (05) both use the word "Done," and it turns out the dictionary only needs that one string filled in once, not once per item, for both to pass; once that clicked, the rest of the roughly seventy strings across all nine demos got organized the same way, by exact text rather than by which item happened to use it. The zip sitting behind the download password usrg4zqs ships every one of these fixes.
Accessibility (reduced-motion)
All nine turn off their decorative auto-play loops under prefers-reduced-motion: reduce (the kebab menu auto-opening, the feed item sliding in, today's pulsing ring, the ghost card, the step checks lighting up in sequence, the presence ping, the palette opening, the new tag chip, the upload progress) while leaving the real state untouched. The kebab menu (01) and the command palette (07) use role="menu"/role="dialog" with aria-expanded/aria-modal and close on an outside click or Escape, and the calendar's (03) day buttons report selection with aria-pressed. The search field in the team list (06) and the remove buttons in the tag manager (08) each carry an aria-label describing what they search or delete, and the layout math behind the trap above is documented on MDN's Element.scrollHeight page.
More dashboard pieces live in the dashboard category, and what this site covers overall is on the about page.
FAQ
Can I use just one of these instead of a full admin dashboard?
Yes, that's the reason they're split up this way. Each demo keeps a single widget centered on its own page instead of a full admin layout, and the zip only ships that one piece's HTML, SCSS, and JS, so you can drop it straight into your own header or sidebar slot.
Does the React version actually hold state too?
Yes. Anything stateful — the command palette opening and closing, the calendar's selected day, the tag list — is held in useState, and the three variables (duration, easing, color) come in as props and get passed through as CSS custom properties.
Is the file upload or the progress bar just for show?
No, both actually run. The file upload (09) really adds the picked file's name to the list and fills a progress bar from zero to full, and the goal tracker (05) button really fills the bar to the next step's value every time you click it. The looping motion you see in the autoplay GIF is a decorative loop that only runs while nobody has interacted yet, and it stops the instant you click.