Lunia DS

Base components

Avatar

A photo, initials or user icon for a person or a workspace, with an optional status badge, that fades the photo in once it loads and lifts each face a little on hover inside a group.

Loading preview

A row of the six sizes with a photo, initials and an icon fallback, an online and a verified status, and an AvatarGroup of eight people with max 5 and an add button that adds a new person. Replay reloads the photos so they fade in again, and adds one person to the group.

Installation

Run this in your app after the one-time setup on the Getting started page.

$ npx @ganisigit/lunia-ds add avatar

Usage

The demo above, as code.

demo.tsx
'use client';

import * as React from 'react';
import { Avatar, AvatarGroup, AvatarLabelGroup } from './avatar';

// Self-drawn placeholder portraits (a soft head and shoulders on a tinted card), so the demo needs no photos.
const portrait = (bg: string, fg: string) =>
  `data:image/svg+xml,${encodeURIComponent(
    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" fill="${bg}"/><circle cx="32" cy="26" r="11" fill="${fg}"/><path d="M10 64c2-13 11-21 22-21s20 8 22 21z" fill="${fg}"/></svg>`,
  )}`;

const PHOTOS = [
  portrait('#d9ccf5', '#8b6fd6'),
  portrait('#f7d9c4', '#d08a5c'),
  portrait('#c9e4dc', '#5f9e8a'),
  portrait('#f3d0d8', '#c4677f'),
  portrait('#cfdcf2', '#5d82c4'),
  portrait('#efe3c2', '#b4974a'),
];

const PEOPLE = [
  'Olivia Rhye',
  'Phoenix Baker',
  'Lana Steiner',
  'Demi Wilkinson',
  'Candice Wu',
  'Natali Craig',
  'Drew Cano',
  'Orlando Diggs',
];
const NEW_PEOPLE = ['Andi Lane', 'Kate Morrison', 'Koray Okumus'];
const photoOf = (person: string) => PHOTOS[[...PEOPLE, ...NEW_PEOPLE].indexOf(person) % PHOTOS.length];

export default function AvatarDemo({ replay = 0 }: { replay?: number }) {
  const [group, setGroup] = React.useState(PEOPLE);

  const add = React.useCallback(() => {
    setGroup((list) => {
      const next = NEW_PEOPLE.find((n) => !list.includes(n));
      return next ? [next, ...list] : list.slice(1);
    });
  }, []);

  React.useEffect(() => {
    if (replay === 0) return;
    setGroup(PEOPLE);
    const timer = window.setTimeout(add, 500);
    return () => window.clearTimeout(timer);
  }, [replay, add]);

  return (
    // The key remounts the avatars on Replay, so the photos load and fade in again.
    <div key={replay} className="flex flex-col items-center gap-10">
      <div className="flex flex-wrap items-end justify-center gap-4">
        <Avatar size="xs" src={PHOTOS[0]} name="Olivia Rhye" />
        <Avatar size="sm" src={PHOTOS[1]} name="Phoenix Baker" status="online" />
        <Avatar size="md" src={PHOTOS[2]} name="Lana Steiner" status="verified" />
        <Avatar size="lg" name="Demi Wilkinson" status="offline" />
        <Avatar size="xl" placeholder="icon" name="Candice Wu" border />
        <Avatar size="2xl" src={PHOTOS[3]} name="Natali Craig" count={3} />
      </div>
      <div className="flex flex-wrap items-center justify-center gap-4">
        <Avatar shape="square" size="lg" src={PHOTOS[4]} name="Studio Lumen" />
        <Avatar shape="square" size="lg" name="Studio Lumen" border />
        <Avatar shape="square" size="lg" placeholder="icon" border />
      </div>
      <AvatarGroup size="md" max={5} onAdd={add}>
        {group.map((person) => (
          <Avatar key={person} name={person} src={photoOf(person)} />
        ))}
      </AvatarGroup>
      <div className="flex flex-wrap items-center justify-center gap-8">
        <AvatarLabelGroup
          size="md"
          src={PHOTOS[0]}
          name="Olivia Rhye"
          supportingText="olivia@ganisigit.com"
        />
        <AvatarLabelGroup
          size="lg"
          name="Phoenix Baker"
          status="online"
          supportingText="phoenix@ganisigit.com"
        />
      </div>
    </div>
  );
}

API

Parts: root (Avatar), image, fallback (initials or user icon), border ring, status badge (online dot, offline dot, verified tick, company logo or count), group (AvatarGroup): list of avatars that overlap, group more count (+n), group add button (dashed), label group (AvatarLabelGroup): avatar, name, supporting text.

PropTypeDefaultDescription
srcstringundefinedPhoto URL. While it loads, or when it fails, the fallback shows.
altstringundefinedAccessible name of the avatar. Defaults to name.
namestringundefinedPerson or workspace name. Gives the initials (first letters of the first two words) and the default alt.
size"xs" | "sm" | "md" | "lg" | "xl" | "2xl""md"24, 32, 40, 48, 56 or 64px. AvatarGroup takes xs, sm or md.
shape"circle" | "square""circle"Circle for people; square with rounded corners for accounts and workspaces.
borderbooleanfalseAdds a thin ring around the avatar.
placeholder"text" | "icon""text"What the fallback shows: the initials, or a user icon. With no name, the icon always shows.
status"online" | "offline" | "verified"undefinedStatus badge in the bottom-right corner. Also added to the accessible name.
badgeSrcstringundefinedCompany logo in the corner badge (Figma status Type Avatar). Ignored when status is set.
countnumberundefinedNumber in the corner badge (Figma status Type Count), for example unread messages.
maxnumber5AvatarGroup only: how many avatars show before the +n count.
groupLabelstring"People"AvatarGroup only: accessible name of the list.
supportingTextstringundefinedAvatarLabelGroup only: second line under the name, for example an email address. Hidden when not set.
EventDescription
loadingStatusChangeThe photo started loading, loaded or failed. Payload: "loading" | "loaded" | "error".
addAvatarGroup only: the add button was pressed. The add button renders only when this event has a listener.
SlotDescription
defaultAvatarGroup: the Avatar items. Avatar: replaces the fallback content.
badgeCustom content for the corner badge.

Motion

Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.

IDTriggerElementPropertyFrom → toTimingReduced motion
AVT1photo loadedimageopacity0 → 1duration-md (260)ease-outShows at once
AVT2photo still loading after the delay, or failedfallbackopacity0 → 1duration-sm (200)ease-outdelay duration-2xs (120)Shows at once
AVT3status changes (online and offline)status badgebackground-colorold status color → new status color (fg-success-secondary or fg-quaternary)duration-sm (200)ease-outSwaps at once
AVT4status badge appears after first renderstatus badgeopacity, scale0, scale-swap → 1, 1duration-md (260)ease-outShows at once
AVT5hover or keyboard focus on an avatar in a groupavatartranslate y0 → -distance-riseduration-sm (200)ease-outStays still
AVT6pointer or focus leaves the avataravatartranslate y-distance-rise → 0duration-2xs (120)ease-outStays still
AVT7an avatar is added to a groupnew avataropacity, scale0, scale-press → 1, 1duration-md (260)ease-outShows at once
AVT8an avatar is removed from a groupleaving avataropacity, scale1, 1 → 0, scale-pressduration-xs (160)ease-exitHides at once
AVT9hover the add buttonadd buttonbackground-color, border-color, icon colorbg-primary, border-primary, fg-quaternary → bg-primary_hover, border-primary, fg-quaternary_hoverduration-sm (200)ease-outSwaps at once
AVT10keyboard focus on the add button or a linked avatarfocus ringbox-shadownone → focus ring (2px bg-primary gap, 4px focus-ring)duration-2xs (120)ease-outShows at once

Accessibility

  • With a photo, the image has alt set to alt or name; with no photo, the root has role img and aria-label, and the initials or icon are aria-hidden.
  • Status is part of the name, not color only: for example "Olivia Rhye, online" or "Olivia Rhye, verified".
  • A count badge adds its number to the name, for example "Olivia Rhye, 3 new"; the caller sets the words with the badge slot when the count means something else.
  • AvatarGroup is a list (ul and li) with aria-label groupLabel; hidden people are in the +n item, named "5 more people".
  • The add button is a native button named "Add person"; xs (24px) and larger meet the 24 x 24px target (WCAG 2.5.8).
  • The hover lift is decoration only; avatars that are links or buttons show the focus ring on keyboard focus.
  • Reduced motion: the photo and fallback show at once and nothing lifts or scales.