ui-034

Avatar Group

£10 or the whole Vault, £99

An avatar group for collaborators: overlapping avatars part on hover, one tooltip travels between them, with an overflow chip and roster.

intermediate
5 more details
staggermicro-interactionhover-effectkeyboard-navigationdialog-animation

This element reads better at your own screen size than in the frame below:

Updated

About this component

A facepile at the size real products use it: 28px overlapping avatars in a document row, with presence dots, an overflow chip and a roster popover. Hovering the stack parts it with a staggered elastic settle, and a single tooltip travels between avatars, tweening its position and its width as the name under it changes.

Read the full effect overview

It is built from semantic buttons and initials, so there is no image pipeline, and it ships the states a component actually needs: roving-tabindex keyboard navigation, a roster dialog with focus return and Escape, coarse-pointer targets that are spread rather than overlapped, a join animation that makes room for a new teammate, and a small public API with events. Light and dark themes, every colour a token.

Avatar Group - UI component preview

What's included

10 items
  • Overlapping 28px facepile at the scale Slack, Figma and Linear use it
  • Hovering the stack parts it with a staggered elastic settle, and neighbours push away from the hovered avatar
  • One tooltip travels between avatars, tweening its x and its measured width as the name changes
  • Overflow chip collapses the rest of the team and pops as the count changes
  • Roster popover lists everyone with presence, staggered in, Escape and focus return
  • add() makes room in the pile and springs the new teammate in with a ring ping
  • Roving tabindex, arrow keys, Home/End, and a polite live region for joins
  • Coarse pointers get larger, fully separated targets and no hover-only state
  • facepile:focus / blur / open / close / add events for wiring the rest of your UI
  • Light and dark themes driven entirely by custom properties

Perfect for

4 use cases
  • App bars on documents, boards and files with live collaborators
  • Project and issue rows showing assignees and watchers
  • Dashboards and consoles with on-call or active-team presence
  • Shared review surfaces where who-is-here matters more than who-is-listed

How it works

3 sections

One geometry function, three states

The row reserves the width of its parted state, and every avatar is placed from a single function that takes the item count, the step between avatars and the currently hovered index. Rest, parted and hovered all come out of the same call, so nothing is left holding a stale transform after a hover, a join or a resize. The steps are CSS custom properties the script reads at use time, which is why the geometry is right after a theme switch without any JS branching on the theme name.

The tooltip is one node that moves

Rather than a tooltip per avatar, one element is sized against the new name, then travels to its new position and width while the content crossfades. Tweening the width is what makes moving between a short and a long name read as one object travelling rather than two labels swapping.

Making room for someone

add() inserts the avatar, re-lays the whole row against the new count, and tweens the row's own width so the surrounding chrome slides rather than jumps. The new avatar's disc springs from zero with a back ease while a ring pings out of it; past the visible limit the overflow chip takes the join instead and rolls its number.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Avatar Group opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse UI elements

Or the whole library: the Vault, £99, one payment.

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

2. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

3. Add the component markup:

Copy the [data-facepile] block from index.html (it is marked with <!-- Component starts here -->). It needs:

  • [data-facepile] root, optionally with data-max-visible="5"
  • [data-facepile-pile] > [data-facepile-row] holding one .face button per person
  • .face--more[data-facepile-more] as the last child of the row (the overflow chip)
  • [data-facepile-tip] for the travelling tooltip
  • [data-facepile-panel] for the roster popover, with [data-facepile-list]
  • a role="status" element with [data-facepile-live]

Each .face carries data-name, data-status, data-state (active, idle or call), a data-color key mapped in CSS, and an aria-label. Roster rows are built by the script from those attributes, so there is no second list to keep in sync.

What's Included

  • index.html: the facepile shown at real scale in a document row, with a light/dark toggle
  • assets/style.css: component CSS, the demo's ground furniture and both themes
  • assets/script.js: readable GSAP core source with cleanup, keyboard model and a public API
  • assets/script.min.js: minified production script
  • thumbnail.html: build-only capture state

Using It With Your Own Design

What the effect needs from your markup: the data attributes listed above, and real <button> elements for every avatar and for the overflow chip. Nothing else about the surrounding page matters; the component is display: inline-flex and sits wherever you put it.

What is only the demo's styling: everything named .stage-* (the ground, the document row it sits in, the Share button), the .showcase-toolbar strip and the Google Font link. That is demo furniture, not part of what you bought — delete it. The toolbar's Join, Leave and Overflow buttons are there to drive the public API, nothing more.

CSS the effect depends on: do not put a CSS transform on .face, on [data-facepile-row], on [data-facepile-tip] or on .roster — GSAP owns those, and a CSS transform underneath one is read as a pixel offset it never returns from. Under .has-js the row becomes a positioned stage and each .face is an absolute item; without JS they fall back to a flex row with negative margins, which is why the pile still looks right with JavaScript off. Keep --face-size, --step-rest and --step-open defined on the root: the script reads them at use time and re-reads them after a theme switch or a resize.

Themes

Two themes, one design at two token values: light (the default) and dark. Set the attribute on the component's owning root in your own page, or on <body> in the demo:

Code snippet omitted: it ships with the download.

The demo-only toggle and the ?variant=dark URL parameter change that attribute; neither is part of the component. The JavaScript never reads the theme name — it only reads the three geometry properties, at use time.

Everything is a custom property set in both body[data-variant="light"] and body[data-variant="dark"] in assets/style.css. To wear your own brand, re-value this one list in both blocks:

  • Ground and surfaces: --ground, --ground-2, --raised, --hover
  • Text: --ink, --ink-2, --ink-3
  • Lines, radii, elevation: --line, --line-strong, --radius, --radius-sm, --shadow
  • Accent: --accent, --accent-ink (the focus ring, the join ping, the row's primary action)
  • The pile: --face-size, --step-rest, --step-open, --face-font, --face-ring, --face-shadow, --face-radius, --face-fg, --more-bg, --more-fg, --ping-color
  • Avatar discs: --face-a … --face-h, mapped from each face's data-color. Solid colours, dark enough for the white initials on them
  • Presence: --dot-active, --dot-idle, --dot-call, --dot-ring
  • Tooltip: --tip-bg, --tip-fg, --tip-muted, --tip-radius, --tip-border, --tip-shadow
  • Roster popover: --panel-bg, --panel-border, --panel-radius, --panel-shadow, --panel-row-hover, --panel-rule

A chunkier look wants a bigger --step-open as well as a bigger --face-size; the script re-measures both after the theme switch dispatches a resize.

How It Works

One geometry function. Rest, parted and hovered positions all come from a single pass over the current items, so nothing is left holding a stale transform after a hover, a join or a resize. The row reserves the width of its parted state, so parting the stack never nudges the toolbar around it.

The tooltip is one node. It is measured against the new name, then tweened to a new x and a new width while its content crossfades — one object travelling, rather than two labels swapping.

Making room. add() re-lays the row, tweens the row's own width so surrounding chrome slides instead of jumping, and springs the new avatar's disc in with a ring ping. Past data-max-visible, the overflow chip takes the join and rolls its number instead.

Public API and events. The first instance is exposed for wiring:

Code snippet omitted: it ships with the download.

The root also dispatches facepile:focus, facepile:blur, facepile:open, facepile:close and facepile:add, each with the person in event.detail:

Code snippet omitted: it ships with the download.

Accessibility

  • The row is a role="group" with a roving tabindex: one stop in the tab order, then Left/Right (or Up/Down) to move between people, Home and End for the ends.
  • Enter or Space on any avatar, or on the overflow chip, opens the roster popover on that person.
  • The roster is a role="dialog": focus moves into it, Tab and the arrow keys stay inside it, Escape closes it and focus returns to whatever opened it. A click outside closes it too.
  • Every avatar and the overflow chip are real buttons with accessible names; presence dots are decorative and the status is in the label.
  • Joins and roster selections are announced through a polite role="status" region.
  • Coarse pointers get larger avatars, fully separated targets and no hover-only state — the pile is parted from the start and a tap opens the roster.
  • Under prefers-reduced-motion: reduce the pile parts, the roster opens and the tooltip appears with no animation, and the ambient presence ping is off.

Requirements

  • GSAP 3.12+ (the demo uses 3.15.0; core only)
  • No build step or framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.