Repository navigation
feat(avatar): AvatarGroup with clipped separation and inherited sizing - #904
Conversation
Overlapping avatars are separated by masking each one with its neighbour's circle, so the gap is transparent rather than a coloured ring. The stack keeps its shape on gradients, images and dark surfaces, where a ring tinted with the page background does not. - AvatarGroup, with `topmost` choosing which end stays a whole circle - AvatarGroupCount for the "+N" chip, labelled in muted foreground - Size, overlap, gap and initials nudge read inheritable --avatar-size, --avatar-overlap, --avatar-gap and --avatar-nudge, settable on the avatar, the group or any ancestor - Fallback initials shift out of the clipped edge so they read as centred in what is left of the circle - The cut is an SVG circle composited with mask-composite: subtract, which stays crisp at any avatar size and under browser zoom - An @supports guard degrades to a plain overlap on Chrome 111-119, where mask-composite would otherwise turn the bite into a union Avatar now sizes from --avatar-size and scales its type with it, and the image and fallback follow the root radius through rounded-[inherit], so a rounded avatar no longer repeats its radius. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Swap the arbitrary mask properties for Tailwind's own utilities, which compile to identical CSS: mask-subtract, mask-no-repeat and the mask-size / mask-position functional forms. The supports-[] prefixes stay in bracket form, since an @supports condition takes a raw declaration rather than a utility name. Particles now size avatars through --avatar-size instead of a size-* class, so the fallback type scales with the circle rather than being hand-corrected per call site: - p-select-20 drops size-8 entirely, since it restated the default - p-button-28 and p-skeleton-1 move to --avatar-size, keeping their 24px and 40px boxes while the type follows - p-combobox-19/20, p-select-19 and p-group-23 move to --avatar-size and drop their text-[...] overrides on AvatarFallback No Avatar in the registry now carries a size-* class, and no AvatarFallback carries a text-* class; both dimensions come from the one custom property. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three examples were missing for the new API, so add them and put the gallery in a deliberate order rather than the order things were built. - p-avatar-17 shows a group over a gradient, which is the whole point of clipping instead of ringing - p-avatar-18 shows --avatar-overlap and --avatar-gap being tuned - p-avatar-19 shows --avatar-size set on a container, sizing several avatars at once Particle names are left alone so installed copies keep resolving; only the registry order changes. It now runs single avatars, then status and badges, then groups from simple to tuned. The docs gain a Sizing section stating the rule plainly: one inherited property drives the box, the type and the group geometry, and size-* resizes the box alone. AvatarGroup lists its constraints explicitly — uniform size within a group, badges only on the avatar that stays whole, and the Chrome 111-119 fallback — since those are the things a reader or an agent would otherwise have to infer. Adds a changelog entry with a migration diff and an agent migration prompt covering the size-*, fallback text and ring-stack rewrites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…emos Replace the gradient and custom-geometry examples with the ones people actually reach for. - p-avatar-17 renders an avatar as a button, p-avatar-18 as a link, both through Base UI's render prop with an accessible name and a focus-visible outline - p-avatar-20 pairs a single avatar with a tooltip - p-avatar-21 gives every member of a group its own tooltip TooltipTrigger renders the avatar itself, so the trigger and the avatar are one element. That also means it replaces the avatar's data-slot with tooltip-trigger, which the docs now call out: CSS written against [data-slot=avatar] will not match a tooltipped avatar. The group's clipping is unaffected because it targets direct children rather than that attribute. The link example carries a suppression for useAnchorContent. The rule wants literal children on the anchor, which a render prop can never have, since Avatar supplies them; the rendered anchor does have an image with alt text and an aria-label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page had grown to eighteen previews. Back to the density it had before: Fallback Only, Different Radius and Group Avatars as they were, plus Last Avatar On Top for the one prop AvatarGroup takes. The size scale stays in the Sizing section, where the rule it illustrates is stated. Everything else — icon fallback, button and link avatars, tooltips, status dots, badges, group sizes, parent-driven sizing — remains in the particles gallery, which is where the long tail belongs. The render prop and inherited sizing are still covered in prose and code samples under API Reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Component pages carry a Changelog section pointing at the dated entry, as scroll-area, form, table, calendar, otp-field and toggle-group do. Avatar was missing one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AvatarImage declares keepMounted and onLoadingStatusChange, and AvatarFallback declares delay. The last two were not documented anywhere. They now sit in Prop/Type/Default tables, matching how tooltip and toggle-group present theirs. Avatar and AvatarGroupCount stay prose-only, since neither declares a prop of its own — Avatar now says so explicitly and points at render for button and link avatars. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
I tested this on cal in calcom/cal#11066, copying 1. Derived initials are too small below 32px. After migration, initials are N × 1.5px, so 2. The changelog's stack diff reverses which avatar sits on top. In a 3. Interactive group children need a documented pattern. Six of cal's twelve stacks wrap each avatar in a 4. 5. Groups only overlap horizontally. Cal's app-shell header overlaps the org and user avatars side by side on 6. The new background defaults are visible. The
This matches the changelog, so nothing needs to change upstream. The note about setting those classes back explicitly is the important part, and it might deserve a line in the agent migration prompt about logos specifically. |
Summary
AvatarGroupandAvatarGroupCountare new. Overlapping avatars are separated by masking each one with its neighbour's circle, so the gap is transparent rather than a coloured ring. A stack now holds its shape on gradients, photographs and translucent panels, wherering-2 ring-backgrounddoes not.Avatartakes every dimension from one inherited custom property,--avatar-size(default 32px). Font size scales with it, and a group derives overlap, gap and the initials nudge from it.Technique
The cut is a vector circle composited out of the avatar's own circle:
Both layers are SVG, so the whole silhouette comes from one vector rasterisation. Three earlier approaches were rejected against real rendering:
ring-2 ring-backgroundMasked children also carry
rounded-none. Withborder-radiusleft on, its antialiased rim survivessubtractand leaves a hairline ghost of the full circle — reproduced on a barediv, independent of Avatar.Browser support
mask-compositeis Baseline 2023: Chrome/Edge 120, Firefox 53, Safari 15.4. Against Tailwind v4's own floor (Chrome 111, Safari 16.4, Firefox 128) only Chrome and Edge 111–119 are exposed.There the declaration is dropped and the two layers composite with
add, turning the bite into a union — avatars render as hard-edged squares. An@supportsguard gates both the mask androunded-none, so those browsers get a plain overlap with no gap instead.Visual changes to existing avatars
Existing
AvatarJSX needs no edits, but two defaults changed:AvatarFallbackusesbg-border(black/8%) instead ofbg-muted(black/4%). Every fallback is twice the contrast.Avatarno longer paintsbg-background. Visible only behind a transparent image with no fallback.AvatarImageandAvatarFallbacknow follow the root radius viarounded-[inherit], so a rounded avatar no longer repeats its radius on the fallback.Validation
bunx tsc --noEmitinapps/uibunx biome checkon every changed filebun run registry:validate-depsandregistry:buildinapps/uitopmostmodes, LTR and RTL, light and dark, over page/muted/gradient/photo surfaces, and at 4× browser zoomrender={<button>}/render={<a>}verified to emit realBUTTONandAelements with accessible names; group tooltips verified to open with clipping intactMerge Danger
Door: two-way
Blast Radius:
@coss/avatarand its consumersAny app rendering
AvatarFallbacksees the darker surface after updating. Group clipping is new API, so nothing regresses there. Nine particles across select, combobox, button, skeleton and group moved fromsize-*to--avatar-size; their boxes are unchanged but fallback type now scales, so combobox and select avatars show 7.5px initials where they showed a hand-set 10px.Rollback
Revert the five commits;
avatar.tsxandpackages/ui/src/components/avatar.tsxreturn to the ring-based stack.🤖 Generated with Claude Code