8pt

Avatar Stack

Avatar Stack•Data display
Overview

Overlapping member avatars that fan out on hover with name bubbles and a +N count at the end. Avatars are initials on gradients, no images needed.

Plain HTML, CSS and a little vanilla JS, with no framework dependency. Copy the prompt into Claude Code, Cursor, Trae or Codex and the model adds Avatar Stack to your project in your colors.

Where to use it

Dashboards, data tables, chart cards, comparison tables and status panels.

Install

With Builder, install it with the 8pt CLI or call it directly from an editor connected to 8pt MCP.

$ npm i -g @8pt-dev/cli && 8pt add avatar-stack
mcp › Use 8pt MCP to call get_component("avatar-stack")

Code

Source for Avatar Stack. Motion uses only transform and opacity and respects the Reduce motion setting.

<div class="x-av">
  <div class="x-av__row">
    <button type="button" style="--c1:#7b6bff;--c2:#00c2ff" data-n="Ava Chen">AC</button>
    <button type="button" style="--c1:#ff7a59;--c2:#ffcc4d" data-n="Leo Park">LP</button>
    <button type="button" style="--c1:#21c58e;--c2:#7ef0c4" data-n="Mia Rossi">MR</button>
    <button type="button" style="--c1:#e05bff;--c2:#ff8ad8" data-n="Noah Kim">NK</button>
    <button type="button" style="--c1:#4d74ff;--c2:#8fb0ff" data-n="Zoe Liu">ZL</button>
    <span class="x-av__more">+12</span>
  </div>
  <p class="x-av__cap">17  members editing</p>
</div>
.x-av{display:grid;justify-items:center;gap:14px;color:var(--d-fg,#fff)}
.x-av__row{display:flex;align-items:center;padding:6px 10px}
.x-av__row>*{position:relative;flex:none;display:grid;place-items:center;width:48px;height:48px;margin-left:-14px;border-radius:50%;border:3px solid var(--d-bg,#09090d);font:700 13px/1 var(--d-font,system-ui,sans-serif);transition:transform .4s cubic-bezier(.3,1.3,.5,1)}
.x-av__row>*:first-child{margin-left:0}
.x-av__row button{padding:0;color:#fff;background:linear-gradient(135deg,var(--c1),var(--c2));cursor:pointer}
.x-av__more{background:var(--d-card,#14141c);color:var(--d-muted,#9b9ba8);font-size:12px}
.x-av__row button::after{content:attr(data-n);position:absolute;left:50%;bottom:calc(100% + 8px);padding:5px 8px;border-radius:7px;background:var(--d-fg,#fff);color:var(--d-bg,#000);font:600 11px/1 var(--d-font,system-ui,sans-serif);white-space:nowrap;opacity:0;transform:translate(-50%,4px);transition:opacity .2s,transform .2s;pointer-events:none}
.x-av__row button:hover::after,.x-av__row button:focus-visible::after{opacity:1;transform:translate(-50%,0)}
.x-av__row:hover>*,.x-av__row.open>*{transform:translateX(calc(var(--i) * 10px))}
.x-av__row>*:hover{z-index:2}
.x-av__row button:hover{transform:translateX(calc(var(--i) * 10px)) translateY(-4px)}
.x-av__cap{margin:0;display:flex;align-items:center;gap:8px;font:12.5px/1 var(--d-font,system-ui,sans-serif);color:var(--d-muted,#9b9ba8)}
…
const init = (root) => {
  const row = root.querySelector('.x-av__row');
  [...row.children].forEach((el, i) => el.style.setProperty('--i', i));
  // breathe open/closed once so the preview shows the interaction
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  let t1 = setTimeout(() => row.classList.add('open'), 900), t2 = setTimeout(() => row.classList.remove('open'), 2400);
  return () => { clearTimeout(t1); clearTimeout(t2); };
};
Subscribe to Builder to see the full source.See pricing

More data display components

View all

Agent Chat Window

Mac File Window

Disk Cleanup Panel

Shortcut Keycaps

Compare Table

Orbit Timeline

Kanban Column

Code Diff Card

More components

View all

AI Prompt Composer

Film Strip Carousel

Auto Tab Showcase

Media Bento Grid

Model Pill Marquee

Director Art Cards

Glass Folder

File Preview Cards

Keep exploring