Skip to content

Components: Add Avatar and AvatarGroup private components#75591

Open
dabowman wants to merge 6 commits intoWordPress:trunkfrom
dabowman:add/avatar-component
Open

Components: Add Avatar and AvatarGroup private components#75591
dabowman wants to merge 6 commits intoWordPress:trunkfrom
dabowman:add/avatar-component

Conversation

@dabowman
Copy link
Contributor

Summary

Adds two new private components to @wordpress/components for rendering user avatars, intended for use in real-time collaboration UI and other internal features.

  • Avatar — Circular avatar with Gravatar image support, initials fallback, accent border ring, hover-expand badge with name/label, status dimming with icon overlay, and Tooltip integration.
  • AvatarGroup — Overlapping avatar layout with configurable max and overflow indicator (+N), CSS-based z-index stacking, and role="group" for accessibility.

Both are registered as private APIs via lock(privateApis, { Avatar, AvatarGroup }) — not exposed publicly.

Key design decisions

  • Status is consumer-driven: The status prop accepts any string and applies uniform dimming. A dynamic is-{status} class is added for consumer-specific styling. No built-in status-specific overrides.
  • Badge + Tooltip: Badge mode (hover-expand pill) shows label or name. When label differs from name, a Tooltip also wraps the avatar so the full name is accessible on hover.
  • CSS-only z-index stacking: AvatarGroup uses an SCSS @for loop on :nth-child instead of cloneElement for stacking order.
  • Grid-based badge animation: grid-template-columns: 0fr → 1fr for exact natural width expansion (avoids max-width overshoot). prefers-reduced-motion disables transitions.
  • Design tokens throughout: Uses $components-color-accent, $elevation-x-small, $radius-full, $button-size-compact, $grid-unit-*, etc. — no hardcoded values.

Test plan

  • Unit tests pass (30/30 — 24 Avatar + 6 AvatarGroup)
  • Stylelint clean on both SCSS files
  • ESLint clean on all component/test/story files
  • Pre-commit hooks pass (format, lint:js strict, lint:css, api-docs)
  • Verify Storybook stories render correctly (npm run storybook:dev → Design System/Components/Avatar and AvatarGroup)
  • Verify status-private tag excludes stories from public Storybook

🤖 Generated with Claude Code

dabowman and others added 4 commits February 13, 2026 14:58
Introduces two new private components in @wordpress/components:

- Avatar: A circular avatar image with configurable size and
  optional accent border color, driven by CSS custom properties.
- AvatarGroup: Displays stacked avatars with negative-margin
  overlap and a "+N" overflow indicator when children exceed
  a configurable max.

Both are registered as private APIs for internal core usage
and include Storybook stories and unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change size prop from arbitrary number to 'default' | 'small'
  (32px and 24px, matching $button-size-compact and $button-size-small)
- Default avatar renders with no border; borderColor is fully opt-in
- When borderColor is set, apply 1.5px accent border with white
  inset ring, matching the Figma "has border=true" variant
- Use $radius-full, $border-width-tab, $gray-700, and $white tokens
  to match design variables exactly
- Add overflow: clip on base avatar for borderless image clipping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extends the Avatar component with several new features:

- Initials fallback: derives up to two letters from `name` when no
  `src` image is provided.
- `badge` prop (opt-in): hover-expand pill that reveals the user's
  name or a custom `label` override.
- `status` prop: dims the avatar via luminosity blend mode (`active`
  at 20% opacity, `idle` at 30%) to signal non-default states.
- `statusIndicator` prop: accepts any `IconType` value and renders
  it as a centered overlay when `status` is set.
- Suppresses badge, shadow, and status indicator inside AvatarGroup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make AvatarGroup a pure layout container that no longer suppresses
Avatar styling (shadows, badges, status indicators). Add inline z-index
stacking so the first child renders on top, with hover elevation for
expanding badges. Replace the max-width badge animation with CSS Grid
0fr→1fr interpolation for smooth width transitions without layout bumps.
Add Tooltip for the name when badge mode is inactive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dabowman dabowman requested a review from ajitbohra as a code owner February 16, 2026 22:43
@github-actions github-actions bot added the [Package] Components /packages/components label Feb 16, 2026
@github-actions
Copy link

github-actions bot commented Feb 16, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: dabowman <davidabowman@git.wordpress.org>
Co-authored-by: aduth <aduth@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

- Remove built-in `.is-active` status override; all statuses now get
  uniform dimming so the consumer controls differentiation via the
  dynamic `is-{status}` class.
- Preserve Tooltip on badge avatars when `label` differs from `name`,
  so hovering reveals the full name even when the badge shows a
  contextual label like "You".
- Replace `cloneElement` z-index injection in AvatarGroup with a CSS
  `@for` loop on `:nth-child`, removing the legacy React API and its
  type assertion.
- Add `role="group"` to AvatarGroup for screen reader context;
  consumers provide `aria-label` via props.
- Add `aria-label` to the overflow indicator so "+2" reads as
  "2 more".
- Add MixedBadgeAndTooltip story covering badge, tooltip, and label
  combinations in a group.
- Improve JSDoc: clarify `badge` requires `name`, describe `status`
  as an open-ended styling hook.
- Add CHANGELOG entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dabowman dabowman force-pushed the add/avatar-component branch from 0c186ab to 7d77380 Compare February 16, 2026 22:44
@github-actions
Copy link

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @dabowman! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Feb 16, 2026
@chriszarate chriszarate added [Feature] Real-time Collaboration Phase 3 of the Gutenberg roadmap around real-time collaboration [Type] Task Issues or PRs that have been broken down into an individual action to take labels Feb 17, 2026
@dabowman
Copy link
Contributor Author

This component is needed for Real-time Collaboration and for the Notes features. I made a clean PR here with just the component so we can discuss it in isolation, but I've also added the code to the PR that refines the RTC UI over here #75595 . If need be we can relocate the component code in that PR into the editor package where the RTC UI lives. That way it's not making an addition to the components package if we want more time to review this before making an addition.

@aduth
Copy link
Member

aduth commented Feb 17, 2026

Looping in @WordPress/gutenberg-components here for review and awareness. It looks like an initial version of this landed already in #75595 . It'd be good to get some eyes from the broader components group, though as a private API it gives us some buffer to iterate. Aside from being used in real-time collaboration, I could imagine many use-cases of a component like this for general reusability, and is common in design systems (e.g. we've been using a lot of BaseUI recently, which also includes an Avatar component).

It'd be good to try to stabilize and shift this toward consistency with other components in @wordpress/ui, including prop naming, tokens, composability, etc. I'd recommend either addressing those consistency points here after a rebase, or creating a tracking issue(s) for that work.

@aduth aduth requested a review from a team February 17, 2026 19:23
Comment on lines 44 to 49
/**
* A status string applied to the avatar. When set, the image is
* dimmed to indicate a non-default state. A corresponding
* `is-{status}` class is added for custom styling.
*/
status?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this moves to @wordpress/ui, we may want to consider the reliance on CSS modifier class names like is being introduced here, since the components implemented there heavily depend on CSS modules that have randomly-generated class names, for the expressed purpose of preventing this kind of ad hoc style extensibility (citation needed).

Instead, I feel like if we want to support a stylized effect of dimming, that should be baked-in to the component. Or maybe support some customization through composability (nested components, etc.). It's not entirely clear to me how this is intended to be used, since it doesn't appear to be used yet with real-time collaboration based on what I see in #75595.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it's not being used yet. The addition of the status-based class was mostly just to have the option of customizing things beyond what the component already does. But it makes sense that we don't want that. We also don't really need it for the way this is intended to be used. I think I'll just remove the class generation, especially since it's not being used yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to a basic "dimmed" option. So it's style specific and doesn't add arbitrary css anymore.

Comment on lines +10 to +20
/**
* Name of the user. Used as an accessible label and to generate
* initials when no image is provided.
*/
name?: string;
/**
* Visible text shown in the hover badge. When not provided, `name`
* is used instead. Use this to provide contextual labels like "You"
* without affecting the accessible name or initials.
*/
label?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering about alignment between these props and how we think of the label prop in IconButton, where we intentionally try to discourage mismatch between the accessible name and the tooltip content, though I think the use-case of something like "You" is interesting to consider, and feels similar in purpose to the sort of suffixing that was implemented in IconButton's shortcut support (e.g. maybe still deriving tooltip from name, but allowing the option for additional suffixing detail).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm happy to name things whatever y'all think is best. The intent was just to have one prop that provides the accessible name that's used for the tooltip and generating any fallback initials. This would be the "main" name or label or whatever. In addition to that I wanted to provide a secondary label/name/whatever to use contextually if we need to call a collaborator/user/entity something different. I can see this being useful when agents or other connected agentic things start appearing as collaborators. Instances where an entities exact name is less important than the role they're playing in the given situation. Happy to align that with how the button or other components handle that difference in meaning.

Comment on lines 21 to 27
/**
* Whether to show the hover-expand badge that reveals the user's
* name (or `label`) on hover. Requires `name` to be set.
*
* @default false
*/
badge?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way this affects the visual presentation of the avatar reminds me of how we use variant props in other components (e.g. Button variant "describes the visual style treatment"). Though I think this surfaces the need for documented consistency of how we use these common props.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, making this an enum instead of a bool makes a lot of sense. Then we could have other secondary displays for the component. I saw mentions of other status "badges" like green dots for connected or online, things like that. That stuff might fall under this category that just becomes a broader "variant" prop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed this to a variant enum. badge is currently the only option but we could add more

Comment on lines 28 to 36
/**
* Size of the avatar.
*
* - `'default'`: 32px
* - `'small'`: 24px
*
* @default 'default'
*/
size?: 'default' | 'small';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a systems perspective, I think it'd be helpful if we provided some guidance on when each size should be used, similar to what we do in Button, rather than simply communicating the raw pixel size and expecting someone to figure it out on their own.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, totally. The documentation needs to be more prescriptive here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed the docs here

Comment on lines 37 to 43
/**
* CSS color value for an accent border ring around the avatar.
*
* When not provided, no border is rendered and the hover badge
* and avatar status uses the admin theme color as its background.
*/
borderColor?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting to think how we might want to think about token usage here rather than hard-coded values. In other places we've expressly moved away from supporting hard-coded values in favor of tokens (example). Although in how this is used in real-time collaboration with a bunch of different colors for each collaborator, I'm not sure we can really tokenize this as-is, without either baking in tokens for complementary colors (like what you're exploring in #74875), or wrapping every Avatar with ThemeProvider (excessive), or allowing hard-coded values as you're considering here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, ideally this is locked to tokens, but realistically I think it needs to support arbitrary colors. Especially since different uses might need to generate their own colors. If we could provide a method for doing that from the theme package we might be able to tighten that up. But until then it probably needs to accept hex values.

Update Avatar to match the refined API from the
improve/real-time-collaboration-ui-polish branch:

- Replace `badge` boolean with `variant` enum prop
- Replace `status` string with `dimmed` boolean
- Add contrast-aware badge text color via colord
- Rename class/CSS-variable collisions (has-border-color →
  has-avatar-border-color, --components-avatar-border-color →
  --components-avatar-outline-color)
- Move background-color from root to badge variant
- Move status indicator outside __image for dimmed opacity isolation
- Update stories and tests to match

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
}

// Initials fallback: show name characters when no image.
.components-avatar:not(.has-src) > .components-avatar__image {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go with the initials fallback it'd be worth to loop in the design team, so that we can have a pass on how it'd look like for that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the initials here are following the designs they worked on for Notes and RTC. So it should be in line with what they expect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Real-time Collaboration Phase 3 of the Gutenberg roadmap around real-time collaboration First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Package] Components /packages/components [Type] Task Issues or PRs that have been broken down into an individual action to take

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants