Avatar stack
Overlapping avatars with a count for the rest - and a list a screen reader can read.
pnpm dlx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-avatar-stack.jsonnpx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-avatar-stack.jsonbunx --bun shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-avatar-stack.jsonyarn dlx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-avatar-stack.jsonshadcn ships the overlap, AvatarGroup and AvatarGroupCount. What it leaves to each call site is
the arithmetic: slice the list, count the rest, render +N. That arithmetic is usually written
so that max={4} draws four avatars and a count, a fifth circle, and six people in five slots
renders +1, a circle that hides exactly one avatar the same size as itself.
<script lang="ts">
import AvatarStack from '#lib/components/coral/kit/avatar-stack/avatar-stack.svelte';
const reviewers = [
{ name: 'Amara Diallo', src: 'https://github.com/shadcn.png' },
{ name: 'Wei Zhang' },
{ name: 'Sofia Rossi' },
{ name: "Liam O'Connor" },
{ name: 'Priya Sharma' },
{ name: 'Kenji Tanaka' },
{ name: 'Lucas Andersen' }
];
</script>
<div class="flex flex-col items-center gap-4">
<AvatarStack
items={reviewers}
max={4}
label="Reviewers"
overflowLabel={(count) => `${count} more reviewers`}
/>
<AvatarStack items={reviewers.slice(0, 5)} max={5} size="sm" label="Five fit" />
<AvatarStack items={reviewers.slice(0, 6)} max={5} size="lg" label="Six in five slots" />
</div>What Coral adds
maxis the width. It counts circles, the count included, so the space a layout reserves is the space the stack takes.- Never
+1. When one person would be hidden, they are drawn instead. - A list:
role="list", so a screen reader announces how many people there are before reading them, and each avatar names its person through kit/avatar. - Hidden people are not anonymous. The count is labelled ("3 more") and lists the hidden names on hover.
- Any item shape:
getPersonmaps your own type onto name and photo, so a list of users does not have to be reshaped first.
Listing the rest
<script lang="ts">
import AvatarStack from '#lib/components/coral/kit/avatar-stack/avatar-stack.svelte';
import { AvatarGroupCount } from '#lib/components/ui/avatar/index.js';
import * as Popover from '#lib/components/ui/popover/index.js';
type Member = { id: number; fullName: string; photo?: string };
const members: Member[] = [
{ id: 1, fullName: 'Amara Diallo', photo: 'https://github.com/shadcn.png' },
{ id: 2, fullName: 'Wei Zhang' },
{ id: 3, fullName: 'Sofia Rossi' },
{ id: 4, fullName: "Liam O'Connor" },
{ id: 5, fullName: 'Priya Sharma' },
{ id: 6, fullName: 'Kenji Tanaka' }
];
</script>
<AvatarStack
items={members}
max={3}
label="Project members"
getKey={(member) => member.id}
getPerson={(member) => ({ name: member.fullName, src: member.photo })}
>
{#snippet overflow({ hidden, count, label })}
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<AvatarGroupCount role="listitem">
<button {...props} type="button" aria-label={label} class="size-full rounded-full">
+{count}
</button>
</AvatarGroupCount>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-56">
<ul class="flex flex-col gap-1 text-sm">
{#each hidden as member (member.id)}
<li>{member.fullName}</li>
{/each}
</ul>
</Popover.Content>
</Popover.Root>
{/snippet}
</AvatarStack>The overflow snippet receives the hidden items, the count and its label, enough to put the count in
a popover or a tooltip. Render an AvatarGroupCount so it keeps its place in the overlap.
Import
Composes kit/avatar, which the install brings with it.
<script lang="ts">
import AvatarStack from '#lib/components/coral/kit/avatar-stack/avatar-stack.svelte';
</script>Props
Everything the shadcn avatar group accepts stays available: class, aria-*, ref. On top of
that:
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | - | Who to show, in order. Required. |
max | number | - | Circles drawn, count included. Omit to draw everyone. |
getPerson | (item: T) => { name, src, alt } | identity | Maps an item to what the avatar reads. |
getKey | (item: T, index: number) => unknown | index | Keys each avatar. |
size | 'sm' | 'default' | 'lg' | default | Forwarded to every avatar; the count follows. |
label | string | - | Names the whole stack. |
overflowLabel | (count: number) => string | {count} more | Accessible label for the count. |
avatar | Snippet<[{ item, index }]> | - | Replaces each avatar. |
overflow | Snippet<[{ hidden, count, label }]> | - | Replaces the count. |
overflow.ts
import { split } from '#lib/components/coral/kit/avatar-stack/overflow.js';
split(['Amara', 'Wei', 'Sofia', 'Liam', 'Priya'], 4);
// { visible: ['Amara', 'Wei', 'Sofia'], hidden: ['Liam', 'Priya'] }