Avatar Stack Fan Expand — Hover Group Animation Snippet
Avatar Stack Fan Expand · Animations · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="fe-stage">
<p class="fe-hint">Hover any avatar — the whole stack fans apart around it</p>
<div class="fe-stack" id="feStack"></div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f8fafc; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.fe-stage { display: flex; flex-direction: column; align-items: center; gap: 28px; padding: 24px; }
.fe-hint { font-size: 13px; color: #64748b; text-align: center; max-width: 320px; }
.fe-stack { display: flex; padding: 10px 0; }
.fe-avatar {
width: 48px; height: 48px;
border-radius: 50%;
border: 3px solid #fff;
margin-left: -16px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 800; color: #fff;
box-shadow: 0 2px 8px rgba(15,23,42,0.15);
cursor: pointer;
position: relative;
will-change: transform;
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), z-index 0s, margin-left 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.fe-stack .fe-avatar:first-child { margin-left: 0; }// Hovering one avatar doesn't just lift itself — every OTHER avatar in the
// stack physically slides away from it, proportional to how close it sits in
// the stack order, so the whole row visibly "fans open" around the hovered
// member instead of only reacting locally.
var PEOPLE = [
{ initials: 'AC', c: '#6366f1' },
{ initials: 'MR', c: '#16a34a' },
{ initials: 'PS', c: '#d97706' },
{ initials: 'DA', c: '#dc2626' },
{ initials: 'SM', c: '#7c3aed' },
{ initials: 'LO', c: '#0891b2' },
{ initials: 'KT', c: '#db2777' },
];
var stack = document.getElementById('feStack');
var SPREAD = 20; // px each step of distance pushes a neighbor further out
PEOPLE.forEach(function (p, i) {
var el = document.createElement('div');
el.className = 'fe-avatar';
el.style.background = p.c;
el.style.zIndex = String(PEOPLE.length - i);
el.textContent = p.initials;
el.dataset.index = String(i);
stack.appendChild(el);
});
var avatars = Array.prototype.slice.call(stack.querySelectorAll('.fe-avatar'));
avatars.forEach(function (el, i) {
el.addEventListener('mouseenter', function () {
el.style.zIndex = String(PEOPLE.length + 1);
avatars.forEach(function (other, j) {
if (j === i) {
other.style.transform = 'translateY(-10px) scale(1.12)';
return;
}
var delta = j - i; // negative = to the left, positive = to the right
var dir = delta < 0 ? -1 : 1;
var offset = dir * SPREAD * (1 + (Math.abs(delta) - 1) * 0.4);
other.style.transform = 'translateX(' + offset.toFixed(1) + 'px)';
});
});
el.addEventListener('mouseleave', function () {
el.style.zIndex = String(PEOPLE.length - i);
avatars.forEach(function (other) {
other.style.transform = 'translateX(0) translateY(0) scale(1)';
});
});
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Avatar Stack Fan Expand — Proportional Neighbor Displacement on Single-Avatar Hover
A standard overlapping avatar stack lifts only the single avatar under the cursor, leaving its overlapping neighbors untouched underneath it. This variant treats the whole stack as one connected group: hovering any single avatar pushes every other avatar apart, with avatars farther along the stack order moving proportionally further, so the entire row visibly fans open around whichever member is highlighted rather than just popping one circle forward.
Building the stack from data
An array of PEOPLE objects (initials and a color) is rendered into .fe-avatar elements appended to the stack container, each stamped with a data-index and a descending inline z-index so earlier avatars sit visually on top of later ones at rest — matching the usual overlapping-circle stack look.
The displacement formula
On mouseenter of avatar at index i, every other avatar at index j computes delta = j - i — how far away it sits in the stack order, and in which direction. The direction is just the sign of delta. The push distance is SPREAD * (1 + (Math.abs(delta) - 1) * 0.4) — so the immediate neighbor moves by roughly SPREAD pixels, and each avatar one step further out moves progressively more (a 40% increase per extra step), which is what makes the fan read as opening from a single point rather than every avatar sliding by the exact same fixed amount.
The hovered avatar's own treatment
The avatar actually under the cursor doesn't translate horizontally with the rest — it lifts vertically (translateY(-10px)) and scales up slightly, both to distinguish it as the interaction's focus and to avoid it fighting visually with neighbors sliding away from directly underneath it. Its z-index is also bumped above every other avatar so it renders on top regardless of its original stack position.
Resetting the fan
On mouseleave, every avatar's transform resets to translateX(0) translateY(0) scale(1) and the hovered avatar's z-index returns to its original stack-order value. The cubic-bezier(0.34, 1.56, 0.64, 1) transition applied to transform (and to margin-left, in case you also adjust overlap on hover) includes overshoot, giving both the fan-out and the collapse-back a small elastic snap.
Tuning the spread
Increase SPREAD for a wider fan; decrease the 0.4 multiplier in the offset formula to make the fan-out distance grow more evenly across the stack instead of accelerating toward the outer edges.
Step by step
How to Use
- 1Hover any avatarMove your cursor over any circle in the stack — every other avatar slides apart from it, with farther ones moving proportionally more.
- 2Move between avatarsHover a different avatar and the fan recalculates instantly around the new hovered member.
- 3Move the cursor awayLeave the stack and every avatar springs back to its overlapping resting position.
- 4Edit the people listUpdate the PEOPLE array in the JS panel — initials, colors, and count all drive the rendered avatars automatically.
- 5Adjust the fan widthChange the SPREAD constant and the 0.4 growth multiplier in the offset formula to control how wide the fan opens.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
On hover, every non-hovered avatar computes delta = its own index minus the hovered avatar's index. The push distance is SPREAD * (1 + (Math.abs(delta) - 1) * 0.4) — so the immediate neighbor moves roughly SPREAD pixels, and each avatar further along the stack order moves progressively more.
The hovered avatar lifts vertically and scales up rather than sliding horizontally, so it reads clearly as the focus of the interaction rather than competing visually with the neighbors sliding away underneath it.
At rest, earlier avatars in the PEOPLE array have a higher z-index so they overlap later ones, matching the standard stacked-avatar look. On hover, the hovered avatar's z-index is temporarily bumped above every other avatar so it always renders on top regardless of its position in the array.
Yes. Swap the translateX offset calculation for a translateY offset (and adjust the base stack layout from flex-row to flex-column) to fan the avatars open in a vertical list instead of a horizontal row.
Edit the PEOPLE array in the JS panel — each object needs initials and a color. The render loop and hover logic both read directly from this array, so no other code needs to change.
Yes. Click "JSX" for a React component. Store PEOPLE in state or props, render one element per person with a ref array, and compute the same delta-based offset inside onMouseEnter, applying the resulting transform to each ref rather than triggering a state update per frame.