Skip to content
67 changes: 67 additions & 0 deletions apps/ui/content/docs/(root)/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,73 @@ title: Changelog
description: Breaking changes, migration guides, and notable updates.
---

## October 18, 2026

### Avatar group clipping and inherited sizing

**`AvatarGroup`** and **`AvatarGroupCount`** are new. Overlapping avatars are separated by masking each one with its neighbour's circle, so the gap between them is **transparent rather than a coloured ring**. A stack now keeps its shape on gradients, photographs and translucent panels, where a ring tinted with the page background does not.

```tsx
<AvatarGroup>
<Avatar>…</Avatar>
<Avatar>…</Avatar>
<AvatarGroupCount>+2</AvatarGroupCount>
</AvatarGroup>
```

**`topmost`** picks which end of the stack stays a whole circle — **`"first"`** by default, **`"last"`** for the conventional order.

**`Avatar`** now takes every dimension from one inherited custom property, **`--avatar-size`** (default 32px). Font size scales with it, and a group derives its overlap, gap and initials nudge from it. Set it on the avatar, the group, or any ancestor:

```tsx
<div className="[--avatar-size:--spacing(10)]">{/* every avatar below is 40px */}</div>
```

**`--avatar-overlap`**, **`--avatar-gap`** and **`--avatar-nudge`** tune a group and inherit the same way.

Two defaults changed: **`AvatarFallback`** uses **`bg-border`** instead of **`bg-muted`**, which is twice the contrast, and **`Avatar`** no longer paints **`bg-background`** behind its content. **`AvatarImage`** and **`AvatarFallback`** now follow the root radius through **`rounded-[inherit]`**, so a rounded avatar no longer has to repeat its radius on the fallback.

Browsers without **`mask-composite`** (Chrome and Edge 111–119) fall back to a plain overlap with no gap. Nothing else degrades.

**Migration:**

Existing **`Avatar`** JSX requires no changes. A **`size-*`** class still resizes a single avatar, because tailwind-merge replaces the base size utility — but it leaves **`--avatar-size`** untouched, so the font size and any group geometry stay at the inherited value. Prefer the property:

```diff
- <Avatar className="size-10">
+ <Avatar className="[--avatar-size:--spacing(10)]">
```

Once an avatar is sized this way, a hand-tuned font size on its fallback is usually redundant:

```diff
- <AvatarFallback className="text-[10px]">JD</AvatarFallback>
+ <AvatarFallback>JD</AvatarFallback>
```

Replacing a hand-rolled ring stack with **`AvatarGroup`** means dropping the negative spacing and the ring utilities:

```diff
- <div className="flex -space-x-2">
- <Avatar className="ring-2 ring-background">…</Avatar>
- </div>
+ <AvatarGroup>
+ <Avatar>…</Avatar>
+ </AvatarGroup>
```

If you relied on the lighter **`bg-muted`** fallback or on **`bg-background`** behind a transparent image, set those explicitly on your own instances.

```bash
npx shadcn@latest add @coss/avatar
```

**Agent migration prompt:**

```text
Update the local coss Avatar component to the version with AvatarGroup, then migrate call sites. Install with `npx shadcn@latest add @coss/avatar`. Avatar now derives its box, font size and group geometry from one inherited custom property, --avatar-size (default 32px); --avatar-overlap, --avatar-gap and --avatar-nudge tune groups and inherit the same way. Replace size-* classes on Avatar with [--avatar-size:--spacing(N)] using the same N, and set it on a shared ancestor when several avatars share a size. After converting, delete hand-tuned text-* classes on AvatarFallback unless the design needs a size other than the derived one — the derived size is --avatar-size * 0.375. Replace hand-rolled overlapping stacks (flex with -space-x-* plus ring-2 ring-background on each avatar) with AvatarGroup, removing the negative spacing and ring utilities; add topmost="last" if the original kept the last avatar whole. Use AvatarGroupCount for a trailing "+N" chip instead of an Avatar with a fallback. Inside a group every avatar must be the same size, so set --avatar-size on the group, never on individual avatars. A status dot or badge on a clipped avatar is clipped with it; keep those on the avatar that stays whole, and place them in a relative wrapper as a sibling of Avatar rather than inside it. Note two visual default changes to review: AvatarFallback uses bg-border instead of bg-muted, and Avatar no longer paints bg-background. Run formatting and typechecking, then visually check avatars at every size the app uses, including any on gradients or images.
```

## August 19, 2026

### Tabs sizing and segmented control styles
Expand Down
115 changes: 107 additions & 8 deletions apps/ui/content/docs/components/avatar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ npm install @base-ui/react
## Usage

```tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@/components/ui/avatar"
```

```tsx
Expand All @@ -74,34 +80,127 @@ Use `keepMounted` when the image relies on native lazy loading or an optimized i
</Avatar>
```

## Sizing

Every dimension of an avatar comes from one inherited custom property, `--avatar-size`. It defaults to 32px, and the font size, the group overlap, the gap and the initials nudge are all derived from it.

Set it on the avatar, on a group, or on any ancestor:

```tsx
<Avatar className="[--avatar-size:--spacing(12)]">…</Avatar>

<AvatarGroup className="[--avatar-size:--spacing(6)]">…</AvatarGroup>

<div className="[--avatar-size:--spacing(10)]">
{/* every avatar below is 40px */}
</div>
```

A `size-*` utility still resizes a single avatar, because tailwind-merge replaces the base size utility. It does **not** update `--avatar-size`, so the font size and any group geometry stay at the inherited value. Prefer `--avatar-size` and reach for `size-*` only when you want the box to change and nothing else.

<ComponentPreview name="p-avatar-3" />

Because the property is inherited, one declaration on a container sizes every avatar inside it.

## API Reference

### Avatar

Root component. Styled wrapper for `Avatar.Root` from Base UI with default size of 32px (8 units) and rounded-full styling.
Root component. Styled wrapper for `Avatar.Root` from Base UI, round by default and sized from `--avatar-size`.

It declares no props of its own; everything from `Avatar.Root` passes through, including `className` and `render`.

It renders a `<span>`. To make an avatar interactive, use `render` rather than wrapping it:

```tsx
<Avatar render={<button type="button" />}>…</Avatar>
<Avatar render={<a href="/people/ada" />}>…</Avatar>
```

An interactive avatar needs an accessible name. `AvatarImage`'s `alt` supplies one; a fallback-only avatar does not, so add `aria-label`.

### AvatarImage

Image element for the avatar. Styled wrapper for `Avatar.Image` from Base UI with support for `keepMounted` images.
Image element. Styled wrapper for `Avatar.Image` from Base UI.

| Prop | Type | Default | Description |
| ----------------------- | ------------------------------------- | ------- | -------------------------------------------------------------------------------------------- |
| `keepMounted` | `boolean` | `false` | Keep the image mounted and let it load in place, for `loading="lazy"` and `next/image` |
| `onLoadingStatusChange` | `(status: ImageLoadingStatus) => void` | - | Fired when loading status changes: `"idle"`, `"loading"`, `"loaded"` or `"error"` |

### AvatarFallback

Fallback content displayed when the image fails to load. Styled wrapper for `Avatar.Fallback` from Base UI with muted background.
Shown while the image loads or when it fails, and whenever no image is given. Base UI unmounts it once the image loads.

| Prop | Type | Default | Description |
| ------- | -------- | ------- | ------------------------------------------------------ |
| `delay` | `number` | `0` | Milliseconds to wait before showing the fallback |

It inherits its font size from the avatar, so it scales automatically. Override it on the fallback when a specific size is needed:

```tsx
<AvatarFallback className="text-[10px]">JD</AvatarFallback>
```

### AvatarGroup

Lays out overlapping avatars. Each avatar is masked with a circle matching its neighbour, so the avatars are separated by a **transparent gap rather than a ring**. The stack therefore holds its shape on gradients, images and translucent panels, where a ring tinted with the page background would not.

| Prop | Type | Default | Description |
| --------- | ------------------- | --------- | -------------------------------------------- |
| `topmost` | `"first" \| "last"` | `"first"` | Which end of the stack stays a whole circle. |

Geometry is derived from the inherited `--avatar-size`, so sizing a stack is the same gesture as sizing one avatar. Three further properties tune the layout, and all of them inherit:

| Property | Default | Description |
| ------------------ | -------------------------------- | ---------------------------------------- |
| `--avatar-overlap` | `calc(var(--avatar-size) * 0.3)` | How far each avatar sits over the previous one. |
| `--avatar-gap` | `calc(var(--avatar-size) / 16)` | Thickness of the transparent gap. |
| `--avatar-nudge` | `calc((var(--avatar-overlap) + var(--avatar-gap)) * 0.3)` | How far initials shift out of the clipped edge. Set to `0` to centre them geometrically. |

```tsx
<AvatarGroup className="[--avatar-size:--spacing(12)] [--avatar-overlap:--spacing(7)]">
…
</AvatarGroup>
```

**Constraints worth knowing:**

- Every avatar in a group must be the same size. The mask is positioned from the group's `--avatar-size`, so a per-avatar override misplaces the cut. Set the size on the group.
- A status dot or badge on a clipped avatar is clipped with it, and is cropped to the circle. Put one on the avatar that stays whole — the first with `topmost="first"`, the last with `topmost="last"`.
- Browsers without `mask-composite` (Chrome and Edge 111–119) fall back to a plain overlap with no gap. Nothing else degrades.

### AvatarGroupCount

The "+N" chip that ends a group. It is an `Avatar` with a fallback, so it is sized, overlapped and clipped like its neighbours; its label uses `text-muted-foreground`.

```tsx
<AvatarGroup>
<Avatar>…</Avatar>
<AvatarGroupCount>+2</AvatarGroupCount>
</AvatarGroup>
```

## Examples

### Fallback Only

<ComponentPreview name="p-avatar-2" />

### Different Sizes

<ComponentPreview name="p-avatar-3" />

### Different Radius

<ComponentPreview name="p-avatar-4" />

### Group Avatars

<ComponentPreview name="p-avatar-5" />

### Last Avatar On Top

Set `topmost="last"` for the conventional order, where the last avatar is the one left intact.

<ComponentPreview name="p-avatar-16" />

## Changelog

- [October 18, 2026](/ui/docs/changelog#avatar-group-clipping-and-inherited-sizing) — **`AvatarGroup`** and **`AvatarGroupCount`** added; overlapping avatars are separated by a transparent clipped gap instead of a ring. **`Avatar`** now derives its box, type and group geometry from the inherited **`--avatar-size`**. **`AvatarFallback`** moves from **`bg-muted`** to **`bg-border`**, and **`Avatar`** no longer paints **`bg-background`**.
2 changes: 1 addition & 1 deletion apps/ui/public/r/avatar.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"files": [
{
"path": "registry/default/ui/avatar.tsx",
"content": "\"use client\";\n\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\nimport type React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport function Avatar({\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n <AvatarPrimitive.Root\n className={cn(\n \"relative isolate inline-flex size-8 shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-background align-middle font-medium text-xs\",\n className,\n )}\n data-slot=\"avatar\"\n {...props}\n />\n );\n}\n\nexport function AvatarImage({\n className,\n ...props\n}: AvatarPrimitive.Image.Props): React.ReactElement {\n return (\n <AvatarPrimitive.Image\n className={cn(\n \"absolute inset-0 z-10 size-full object-cover data-error:invisible data-loading:invisible\",\n className,\n )}\n data-slot=\"avatar-image\"\n {...props}\n />\n );\n}\n\nexport function AvatarFallback({\n className,\n ...props\n}: AvatarPrimitive.Fallback.Props): React.ReactElement {\n return (\n <AvatarPrimitive.Fallback\n className={cn(\n \"absolute inset-0 flex size-full items-center justify-center rounded-full bg-muted\",\n className,\n )}\n data-slot=\"avatar-fallback\"\n {...props}\n />\n );\n}\n\nexport { AvatarPrimitive };\n",
"content": "\"use client\";\n\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\nimport type React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nconst AVATAR_GROUP_CUT =\n \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'%3E%3Ccircle cx='1' cy='1' r='1'/%3E%3C/svg%3E\\\")\";\n\nexport function Avatar({\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n <AvatarPrimitive.Root\n className={cn(\n \"relative isolate inline-flex size-(--avatar-size,--spacing(8)) shrink-0 select-none items-center justify-center overflow-hidden rounded-full align-middle font-medium text-[calc(var(--avatar-size,--spacing(8))*0.375)]/none\",\n className,\n )}\n data-slot=\"avatar\"\n {...props}\n />\n );\n}\n\nexport function AvatarImage({\n className,\n ...props\n}: AvatarPrimitive.Image.Props): React.ReactElement {\n return (\n <AvatarPrimitive.Image\n className={cn(\n \"absolute inset-0 z-10 size-full rounded-[inherit] object-cover data-error:invisible data-loading:invisible\",\n className,\n )}\n data-slot=\"avatar-image\"\n {...props}\n />\n );\n}\n\nexport function AvatarFallback({\n className,\n ...props\n}: AvatarPrimitive.Fallback.Props): React.ReactElement {\n return (\n <AvatarPrimitive.Fallback\n className={cn(\n \"absolute inset-0 flex size-full items-center justify-center rounded-[inherit] bg-border\",\n className,\n )}\n data-slot=\"avatar-fallback\"\n {...props}\n />\n );\n}\n\nexport interface AvatarGroupProps extends React.ComponentProps<\"div\"> {\n topmost?: \"first\" | \"last\";\n}\n\nexport function AvatarGroup({\n className,\n style,\n topmost = \"first\",\n ...props\n}: AvatarGroupProps): React.ReactElement {\n return (\n <div\n className={cn(\n \"flex w-fit items-center\",\n \"[--avatar-group-size:var(--avatar-size,--spacing(8))]\",\n \"[--avatar-group-gap:var(--avatar-gap,calc(var(--avatar-group-size)/16))]\",\n \"[--avatar-group-overlap:var(--avatar-overlap,calc(var(--avatar-group-size)*0.3))]\",\n \"[--avatar-group-cut-end:calc(var(--avatar-group-size)*1.5-var(--avatar-group-overlap))]\",\n \"[--avatar-group-cut-radius:calc(var(--avatar-group-size)/2+var(--avatar-group-gap))]\",\n \"[--avatar-group-cut-start:calc(var(--avatar-group-overlap)-var(--avatar-group-size)/2)]\",\n \"[--avatar-group-mask-position:0_0,calc(var(--avatar-group-cut-x)-var(--avatar-group-cut-radius))_50%]\",\n \"[--avatar-group-mask-size:100%_100%,calc(var(--avatar-group-cut-radius)*2)_calc(var(--avatar-group-cut-radius)*2)]\",\n \"[--avatar-group-mask:none]\",\n \"supports-[mask-composite:subtract]:[--avatar-group-mask:var(--avatar-group-cut-image),var(--avatar-group-cut-image)]\",\n \"[--avatar-group-nudge:var(--avatar-nudge,calc((var(--avatar-group-overlap)+var(--avatar-group-gap))*0.3))]\",\n \"*:not-first:-ms-(--avatar-group-overlap)\",\n topmost === \"first\"\n ? \"*:not-first:mask-(--avatar-group-mask) *:not-first:mask-subtract *:not-first:mask-position-(--avatar-group-mask-position) *:not-first:mask-no-repeat *:not-first:mask-size-(--avatar-group-mask-size) [--avatar-group-cut-x:var(--avatar-group-cut-start)] *:not-first:*:data-[slot=avatar-fallback]:ps-[calc(var(--avatar-group-nudge)*2)] supports-[mask-composite:subtract]:*:not-first:rounded-none rtl:[--avatar-group-cut-x:var(--avatar-group-cut-end)]\"\n : \"*:not-last:mask-(--avatar-group-mask) *:not-last:mask-subtract *:not-last:mask-position-(--avatar-group-mask-position) *:not-last:mask-no-repeat *:not-last:mask-size-(--avatar-group-mask-size) [--avatar-group-cut-x:var(--avatar-group-cut-end)] *:not-last:*:data-[slot=avatar-fallback]:pe-[calc(var(--avatar-group-nudge)*2)] supports-[mask-composite:subtract]:*:not-last:rounded-none rtl:[--avatar-group-cut-x:var(--avatar-group-cut-start)]\",\n className,\n )}\n data-slot=\"avatar-group\"\n data-topmost={topmost}\n style={\n {\n \"--avatar-group-cut-image\": AVATAR_GROUP_CUT,\n ...style,\n } as React.CSSProperties\n }\n {...props}\n />\n );\n}\n\nexport function AvatarGroupCount({\n children,\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n <Avatar\n className={cn(\"text-muted-foreground\", className)}\n data-slot=\"avatar-group-count\"\n {...props}\n >\n <AvatarFallback>{children}</AvatarFallback>\n </Avatar>\n );\n}\n\nexport { AvatarPrimitive };\n",
"type": "registry:ui"
}
],
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/public/r/p-avatar-11.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"files": [
{
"path": "registry/default/particles/p-avatar-11.tsx",
"content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\n\nexport default function Particle() {\n return (\n <div className=\"relative\">\n <Avatar className=\"rounded-lg\">\n <AvatarImage\n alt=\"User\"\n src=\"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80\"\n />\n <AvatarFallback className=\"rounded-lg\">LT</AvatarFallback>\n </Avatar>\n <Badge\n className=\"absolute -end-1.5 -top-1.5 rounded-full outline-2 outline-background outline-solid\"\n size=\"sm\"\n >\n 6\n </Badge>\n </div>\n );\n}\n",
"content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\n\nexport default function Particle() {\n return (\n <div className=\"relative\">\n <Avatar className=\"rounded-lg\">\n <AvatarImage\n alt=\"User\"\n src=\"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80\"\n />\n <AvatarFallback>LT</AvatarFallback>\n </Avatar>\n <Badge\n className=\"absolute -end-1.5 -top-1.5 rounded-full outline-2 outline-background outline-solid\"\n size=\"sm\"\n >\n 6\n </Badge>\n </div>\n );\n}\n",
"type": "registry:block"
}
],
Expand Down
Loading
Loading