UIDex

Avatar and Avatar Group

الصورة الرمزية ومجموعتها

<img alt> / AvatarGroup

also calledprofile picture، user avatar، initials avatar، avatar stack، facepile، profile photo، أفاتار، الصورة الشخصية، صورة المستخدم، بديل الحروف الأولى، صفّ الصور المتداخلة، أيقونة الحساب

An avatar is the small graphic that identifies one named entity: a person, a team, or a company. It shows a picture when there is one, and falls back to the initials of the name on a coloured ground when there is none or when the file fails to load. The shape carries meaning ahead of any label: a circle for people, a rounded square for organisations, and mixing the two inside one list throws that signal away. An avatar is not an icon, because an icon names an action or a type (delete, folder, settings) and the same drawing is reused everywhere, while an avatar points at one specific entity and is simply wrong on anyone else. It is not a badge: a badge is a count or a state stuck onto another element in order to describe it, whereas the avatar is the element rather than the decoration on it. It is not a chip either, since a chip spells the name out in letters, usually carries a remove button, and lives inside an input. An avatar group is a row of overlapping avatars that answers "who is on this?" at a glance, and it summarises a list instead of replacing one, so put a real list behind the counter when the user needs the names.

If you called it…

"the round profile photo""the two letters that show when there is no picture""the little pictures overlapping in a row""the green dot on the corner of the photo""the circle that says plus three"

Live specimen

Interact with the demo. Every part is real and numbered.

يعملون على «خطة الربع الرابع»

سينر
طارق مسعودمحرّر، متاح الآن
RTLأول صورة عند اليمين، وكل تالية تنزلق تحتها، والعدّاد في نهاية الصفّ

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build an avatar component and an avatar group. Render the picture as a real <img> with object-fit: cover and an alt carrying the name of the person, and leave alt="" only when that name is already printed beside it. Wire onError to swap in the initials fallback so the browser never paints its broken-image glyph, derive the initials from the full name and not from the email address, and separate two Arabic letters with U+200C so the script does not join them into one shape. Use a circle for people and a rounded square for organisations. Add an optional status dot anchored with inset-inline-end: 0 and inset-block-end: 0, give it a ring in the background colour, and carry the state in visible text or an accessible name rather than in colour alone. In the group, show three to five faces, build the overlap with a negative margin-inline-start on every avatar after the first, and give each one a z-index running against DOM order so the first sits on top; do not re-reverse those values inside [dir="rtl"] and do not reach for flex-direction: row-reverse. Render the surplus as a "+N" chip at the logical end of the row, as a real button that opens the full list, with an accessible name saying how many more people it stands for, and isolate the token in a span with dir="ltr" so it does not turn around in Arabic. Use logical properties throughout (margin-inline, inset-inline, padding-inline) and never write left, right, or margin-left. Finally, keep photographs and logos out of any blanket mirroring rule.

How it behaves right-to-left

This section is ours alone.

Which way the row overlaps

mirrors

In English the first avatar sits at the left and each next one slides under it toward the right. In Arabic the whole row turns around: the first avatar is at the right and the row grows to the left. Write the offset as margin-inline-start: -8px on every avatar except the first, so each one is pulled toward whichever side "previous" happens to be on. A margin-left: -8px keeps pulling left after the flip, so each avatar slides under the one that comes after it instead of the one before it, the first two end up flush with no overlap at all, and the last one drags the far edge of the row 8px inward. Do not patch that with a margin-right inside [dir="rtl"]; the logical property already covers both directions.

Stacking order

never mirrors

The look where each avatar tucks under the one before it comes from a z-index that runs against DOM order: the first gets the highest value and the last the lowest, because the browser otherwise paints a later sibling over an earlier one and the effect inverts. Those values stay put when the page flips. z-index is a painting property rather than a layout one and has no logical form at all, so re-reversing it inside [dir="rtl"] lifts the last avatar above everything and the row reads inverted. What does move is the hidden crescent of each circle: it stays at the logical start, sitting on the left edge of each avatar in English and on the right edge in Arabic. Avoid the flex-direction: row-reverse trick with reversed DOM order for getting this look, because row-reverse is resolved against direction and turns the entire Arabic row around.

The status dot corner

mirrors

The dot belongs in the bottom corner at the logical end: bottom right in English, bottom left in Arabic. Anchor it with inset-inline-end: 0 and inset-block-end: 0 rather than right: 0. The trap is the small nudge that pushes the dot out over the edge, because transform has no logical form at all: a positive X in translate(25%, 25%) always moves right, so once the anchor lands on the left side in Arabic the same nudge creeps the dot inward across the face instead of outward. Multiply the horizontal axis by a --dir sign set to -1 under [dir="rtl"], or drop the transform and use inset-inline-end: -2px. If a library places the badge with a value named "left" or "right", that name is physical: MUI resolves anchorOrigin against theme.direction, so check whether yours does the same before you trust it to flip on its own.

The counter and its plus sign

mirrors

The counter stands at the logical end of the row, so it moves from the far right in English to the far left in Arabic. That happens by itself when the row is an ordinary flex container and the chip is last in the DOM. If instead you laid the avatars out with position: absolute and left: calc(i * 20px), move that calc to inset-inline-start or the counter parks on top of the first avatar. The text needs a separate fix from the layout. A plus sign has no direction of its own: U+002B carries bidi class ES, which is weak rather than strong or neutral, so the algorithm hands it the direction of whatever surrounds it and a token typed as "+3" paints as "3+" on an Arabic line. That is the algorithm working correctly and still not what the design drew, so wrap the token in a span with dir="ltr", an attribute the HTML user-agent stylesheet already gives unicode-bidi: isolate, or write the count out in words.

What never mirrors: photo and initials

never mirrors

A photograph is content, not chrome. The blanket rule some teams write to flip icons, [dir="rtl"] img, [dir="rtl"] svg { transform: scaleX(-1) }, catches every face and every logo, including the lettering drawn inside the logo. Scope the flip to a class you put on arrows only. object-position accepts physical keywords only, and here that is the right behaviour, since the crop belongs to the picture rather than to the page: object-position: right center still means the right edge of the file after the flip, which is what you want. The initials are plain text whose order the bidi algorithm already decides, so never reverse the string yourself for Arabic, and centre it with text-align: center so a one letter fallback does not sit somewhere different from a two letter one.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

HTML<img alt="…" onerror>The alt carries the name, and onerror is what puts the initials in place of the broken-image glyph.
ARIArole="img" + aria-labelAn avatar built from a div and two letters is not an image in the accessibility tree until it says so, and role="img" hides the letters inside it, which leaves aria-label carrying the whole name.
Radix UI<Avatar.Image> + <Avatar.Fallback delayMs>The fallback waits delayMs before painting, so the initials do not flash on a fast connection.
shadcn/ui<AvatarImage> + <AvatarFallback>The fallback is a sibling element rather than a prop, so leaving it empty gives you a blank circle on failure.
Material UI<AvatarGroup max={4}>The number includes the surplus chip itself, so max={4} shows three faces and a +N.
Ant Design<Avatar.Group maxCount={3}>Here the number counts the faces only, so the same value gives you one more circle than Material does.
CSSmargin-inline-start: -0.5remThe negative logical margin is what makes the overlap follow page direction with no RTL override.

See also

Updated · 2026-08-19