GODRICH

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

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.

transform-originclickaria-expanded
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.

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

Dateclickaria-pressed
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.

transformappendChildclick
@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.

scaleXtransform-originclick
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.

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

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

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

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

Enter the archive password

The password is inside this article. You will find it as you read.