HextaUI

Avatar

Des photos d’utilisateurs avec initiales en repli, badges de statut et groupes empilés qui se réduisent en compteur.

ALGH
ALATGH2 more
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
} from "@/components/ui/avatar"

export function AvatarDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-8">
      <Avatar size="xl">
        <AvatarImage src="/preview/landscape.svg" alt="" />
        <AvatarFallback>AL</AvatarFallback>
        <AvatarBadge status="online" />
      </Avatar>
      <Avatar size="xl" shape="square">
        <AvatarFallback>GH</AvatarFallback>
      </Avatar>
      <AvatarGroup size="lg" max={4}>
        <Avatar>
          <AvatarImage src="/preview/landscape.svg" alt="" />
          <AvatarFallback>AL</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarFallback>AT</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarFallback>GH</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarFallback>KJ</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarFallback>LT</AvatarFallback>
        </Avatar>
      </AvatarGroup>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/avatar.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
} from "@/components/ui/avatar"
<Avatar>
  <AvatarImage src="/ada.jpg" alt="" />
  <AvatarFallback>AL</AvatarFallback>
</Avatar>
Avatar
├── AvatarImage
├── AvatarFallback
└── AvatarBadge

AvatarGroup
├── Avatar
└── AvatarGroupCount

Tailles et formes

Cinq tailles, en cercles ou en carrés. Les initiales et l’icône d’utilisateur suivent la taille de la boîte, et les coins carrés s’adoucissent avec la taille. Un <AvatarFallback /> vide affiche l’icône d’utilisateur.

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"

const sizes = ["xs", "sm", "default", "lg", "xl"] as const
const shapes = ["circle", "square"] as const

export function AvatarSizes() {
  return (
    <div className="flex flex-col gap-4">
      {shapes.map((shape) => (
        <div key={shape} className="flex flex-wrap items-center gap-3">
          {sizes.map((size) => (
            <Avatar key={`photo-${size}`} size={size} shape={shape}>
              <AvatarImage src="/preview/landscape.svg" alt="" />
              <AvatarFallback>AL</AvatarFallback>
            </Avatar>
          ))}
          {sizes.map((size) => (
            <Avatar key={`initials-${size}`} size={size} shape={shape}>
              <AvatarFallback>LT</AvatarFallback>
            </Avatar>
          ))}
          {sizes.map((size) => (
            <Avatar key={`empty-${size}`} size={size} shape={shape}>
              <AvatarFallback />
            </Avatar>
          ))}
        </div>
      ))}
    </div>
  )
}

Chargement

Les initiales s’affichent pendant le chargement de la photo, puis la photo apparaît en fondu par-dessus. Une photo cassée conserve le contenu de repli. Passez delay pour attendre avant d’afficher les initiales, afin que les photos rapides ne les fassent jamais clignoter.

"use client"

import * as React from "react"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"

export function AvatarLoading() {
  const [version, setVersion] = React.useState(0)
  const [loading, setLoading] = React.useState(false)
  const timer = React.useRef<number>(undefined)

  React.useEffect(() => () => window.clearTimeout(timer.current), [])

  const reload = () => {
    window.clearTimeout(timer.current)
    setLoading(true)
    timer.current = window.setTimeout(() => {
      setVersion((value) => value + 1)
      setLoading(false)
    }, 1200)
  }

  return (
    <div className="flex flex-wrap items-center gap-3">
      <Avatar size="xl">
        {loading ? null : (
          <AvatarImage src={`/preview/landscape.svg?v=${version}`} alt="" />
        )}
        <AvatarFallback>AT</AvatarFallback>
      </Avatar>
      <Avatar size="xl">
        <AvatarImage src="/preview/missing.png" alt="" />
        <AvatarFallback>BI</AvatarFallback>
      </Avatar>
      <Avatar size="xl">
        <AvatarImage src="/preview/landscape.svg" alt="" />
        <AvatarFallback delay={600}>GH</AvatarFallback>
      </Avatar>
      <Button variant="outline" size="sm" onClick={reload}>
        Load a new photo
      </Button>
    </div>
  )
}

Initiales

getInitials() retient la première et la dernière initiale. Elle gère les adresses e-mail, les emoji, les noms CJK et RTL, les signes combinants, ainsi que les noms sans aucune lettre.

"use client"

import { Avatar, AvatarFallback, getInitials } from "@/components/ui/avatar"

const names = [
  "Ada Lovelace",
  "Madonna",
  "jean-luc picard",
  "[email protected]",
  "(Admin) John",
  "👩‍👩‍👧‍👦 Family",
  "山田 太郎",
  "محمد علي",
  "Z̷̢̛͖͓̰̈́algo T̵ext",
  "Mary Ann Evans Cross",
  "!!! ???",
  "",
]

export function AvatarInitials() {
  return (
    <ul className="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
      {names.map((name) => (
        <li key={name} className="flex min-w-0 items-center gap-2 text-sm">
          <Avatar>
            <AvatarFallback>{getInitials(name)}</AvatarFallback>
          </Avatar>
          <span className="min-w-0 truncate text-muted-foreground">
            {name || "(empty)"}
          </span>
        </li>
      ))}
    </ul>
  )
}

Statut

<AvatarBadge /> se place sur le bord pour toutes les tailles et formes. Définissez status pour un point coloré avec une étiquette accessible, ou passez une icône. Un changement de statut joue une seule pulsation.

"use client"

import * as React from "react"
import { IconCheck } from "@tabler/icons-react"

import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarImage,
  type AvatarStatus,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"

const sizes = ["xs", "sm", "default", "lg", "xl"] as const
const shapes = ["circle", "square"] as const
const statuses: AvatarStatus[] = ["online", "away", "busy", "offline"]

export function AvatarStatusDemo() {
  const [index, setIndex] = React.useState(0)
  const status = statuses[index]

  return (
    <div className="flex flex-col gap-4">
      {shapes.map((shape) => (
        <div key={shape} className="flex flex-wrap items-center gap-3">
          {sizes.map((size) => (
            <Avatar key={size} size={size} shape={shape}>
              <AvatarImage src="/preview/landscape.svg" alt="" />
              <AvatarFallback>GH</AvatarFallback>
              <AvatarBadge status={status} />
            </Avatar>
          ))}
          <Avatar size="lg" shape={shape}>
            <AvatarFallback>AT</AvatarFallback>
            <AvatarBadge>
              <IconCheck />
            </AvatarBadge>
          </Avatar>
        </div>
      ))}
      <div className="flex items-center gap-3">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setIndex((index + 1) % statuses.length)}
        >
          Next status
        </Button>
        <span className="text-sm text-muted-foreground">{status}</span>
      </div>
    </div>
  )
}

Groupe

<AvatarGroup /> superpose ses avatars et définit leur taille et leur forme. max regroupe le reste en un compteur.

"use client"

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  getInitials,
} from "@/components/ui/avatar"

const people = [
  { name: "Ada Lovelace", image: "/preview/landscape.svg" },
  { name: "Alan Turing", image: "/preview/landscape.svg" },
  { name: "Grace Hopper", image: "/preview/landscape.svg" },
  { name: "Katherine Johnson", image: null },
  { name: "Linus Torvalds", image: null },
  { name: "Margaret Hamilton", image: null },
  { name: "Tim Berners-Lee", image: null },
  { name: "Barbara Liskov", image: null },
]

const sizes = ["xs", "sm", "default", "lg", "xl"] as const

export function AvatarGroupDemo() {
  return (
    <div className="flex flex-col items-start gap-4">
      {sizes.map((size) => (
        <AvatarGroup key={size} size={size} max={4}>
          {people.map((person) => (
            <Avatar key={person.name}>
              {person.image ? <AvatarImage src={person.image} alt="" /> : null}
              <AvatarFallback>{getInitials(person.name)}</AvatarFallback>
            </Avatar>
          ))}
        </AvatarGroup>
      ))}
      <AvatarGroup shape="square" max={5}>
        {people.map((person) => (
          <Avatar key={person.name}>
            {person.image ? <AvatarImage src={person.image} alt="" /> : null}
            <AvatarFallback>{getInitials(person.name)}</AvatarFallback>
          </Avatar>
        ))}
      </AvatarGroup>
    </div>
  )
}

Groupe avec liens

Rendez les avatars en liens avec render et donnez à chacun un aria-label. Un avatar ayant le focus passe au-dessus de ses voisins pour que l’anneau ne soit jamais coupé. Ajoutez <AvatarGroupCount /> vous-même lorsque le total provient de vos données.

"use client"

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarGroupCount,
  getInitials,
} from "@/components/ui/avatar"

const team = [
  "Ada Lovelace",
  "Alan Turing",
  "Grace Hopper",
  "Linus Torvalds",
  "Margaret Hamilton",
]

export function AvatarLinkedGroup() {
  return (
    <AvatarGroup aria-label="Team" size="lg">
      {team.map((name) => (
        <Avatar key={name} render={<a href="#" aria-label={name} />}>
          <AvatarFallback>{getInitials(name)}</AvatarFallback>
        </Avatar>
      ))}
      <AvatarGroupCount count={3} />
    </AvatarGroup>
  )
}

Mise en page

Les avatars ne rétrécissent jamais dans les lignes serrées. Une classe de taille comme size-20 met à l’échelle les initiales et le badge, et de longues initiales ne débordent jamais.

import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"

export function AvatarLayout() {
  return (
    <div className="flex flex-col items-start gap-4">
      <div className="flex w-64 max-w-full items-center gap-2 rounded-lg border p-2 text-sm">
        <Avatar>
          <AvatarImage src="/preview/landscape.svg" alt="" />
          <AvatarFallback>AL</AvatarFallback>
        </Avatar>
        <span className="min-w-0 truncate">
          Supercalifragilisticexpialidocious-team-workspace-name
        </span>
      </div>
      <div className="flex flex-wrap items-center gap-3">
        <Avatar className="size-20">
          <AvatarImage src="/preview/landscape.svg" alt="" />
          <AvatarFallback>KJ</AvatarFallback>
        </Avatar>
        <Avatar className="size-20" shape="square">
          <AvatarFallback>KJ</AvatarFallback>
          <AvatarBadge status="online" />
        </Avatar>
        <Avatar size="sm">
          <AvatarFallback>WWWWWWWW</AvatarFallback>
        </Avatar>
      </div>
    </div>
  )
}

De droite à gauche

Le badge reste sur le coin de fin, c’est-à-dire à gauche en RTL, et les groupes se chevauchent depuis la droite.

"use client"

import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  getInitials,
} from "@/components/ui/avatar"

const people = [
  { name: "Ada Lovelace", image: "/preview/landscape.svg" },
  { name: "Alan Turing", image: "/preview/landscape.svg" },
  { name: "Grace Hopper", image: "/preview/landscape.svg" },
  { name: "Katherine Johnson", image: null },
  { name: "Linus Torvalds", image: null },
  { name: "Margaret Hamilton", image: null },
  { name: "Tim Berners-Lee", image: null },
  { name: "Barbara Liskov", image: null },
]

export function AvatarRtl() {
  return (
    <div dir="rtl" className="flex flex-wrap items-center gap-4">
      <Avatar size="lg">
        <AvatarFallback>{getInitials("محمد علي")}</AvatarFallback>
        <AvatarBadge status="online" />
      </Avatar>
      <AvatarGroup max={4}>
        {people.map((person) => (
          <Avatar key={person.name}>
            {person.image ? <AvatarImage src={person.image} alt="" /> : null}
            <AvatarFallback>{getInitials(person.name)}</AvatarFallback>
          </Avatar>
        ))}
      </AvatarGroup>
    </div>
  )
}

Les avatars ne sont pas focusables d’eux-mêmes. Rendus en lien ou en bouton, ils reçoivent les touches habituelles.

ToucheAction
TabDéplace le focus vers l’avatar lié suivant.
EnterSuit le lien ayant le focus.
  • Utilisez alt="" lorsque le nom de la personne est déjà à côté de l’avatar, et son nom comme texte alternatif dans le cas contraire.
  • Les badges avec un status sont annoncés « Online », « Away », « Busy » ou « Offline ». Offline est dessiné comme un anneau : le statut ne repose donc jamais sur la couleur seule.
  • Les groupes ont role="group". Le compteur se lit « 3 more », et non « +3 ».
  • Avec la réduction des animations, les photos apparaissent sans fondu et les changements de statut ne pulsent pas.

Construit sur l’avatar de Base UI. Chaque partie accepte les attributs de l’élément qu’elle rend. Les styles sont exportés sous avatarVariants et avatarBadgeVariants.

PropTypePar défaut
sizeHérité du groupe si omis.
"xs" | "sm" | "default" | "lg" | "xl""default"
shapeHérité du groupe si omis.
"circle" | "square""circle"
render
ReactElement | (props, state) => ReactElement<span>
AttributDescription
data-slot="avatar"Ciblez les avatars en CSS.
data-sizeLa taille résolue.
data-shapeLa forme résolue.
--avatar-radiusLe rayon des coins, partagé par toutes les couches.
PropTypePar défaut
src
string–
alt
string–
onLoadingStatusChange
(status: "idle" | "loading" | "loaded" | "error") => void–
keepMountedCharge l’image sur place au lieu de la précharger, pour loading="lazy" ou next/image.
booleanfalse
render
ReactElement | (props, state) => ReactElement<img>
AttributDescription
data-slot="avatar-image"Ciblez les images en CSS.
data-loadingPrésent pendant le chargement de l’image.
data-errorPrésent lorsque l’image n’a pas pu se charger.
data-starting-stylePrésent pendant l’apparition en fondu de l’image.
data-ending-stylePrésent pendant la disparition en fondu de l’image.
PropTypePar défaut
childrenVide ou composé d’espaces, il affiche l’icône d’utilisateur.
ReactNode<IconUser />
delayMillisecondes à attendre avant de l’afficher.
number0
render
ReactElement | (props, state) => ReactElement<span>
AttributDescription
data-slot="avatar-fallback"Ciblez les contenus de repli en CSS.
data-readyfalse jusqu’à l’écoulement du délai.
PropTypePar défaut
statusColore le point et l’étiquette pour les technologies d’assistance. Sans lui, le badge utilise la couleur primaire.
"online" | "away" | "busy" | "offline"–
childrenUne icône dans le badge. Masquée aux tailles xs et sm.
ReactNode–
AttributDescription
data-slot="avatar-badge"Ciblez les badges en CSS.
data-statusLe statut actuel.
data-slot="avatar-badge-pulse"La pulsation jouée après un changement de statut.
PropTypePar défaut
size
"xs" | "sm" | "default" | "lg" | "xl""default"
shape
"circle" | "square""circle"
maxNombre d’éléments à afficher, compteur compris. Les valeurs inférieures à 2 sont portées à 2.
number–
AttributDescription
data-slot="avatar-group"Ciblez les groupes en CSS.
data-sizeLa taille du groupe.
PropTypePar défaut
countAffiché comme +3, ou 99+ au-delà de 99.
number–
childrenRemplace le compteur, par exemple par une icône.
ReactNode–
sizeHérité du groupe si omis.
"xs" | "sm" | "default" | "lg" | "xl"–
shapeHérité du groupe si omis.
"circle" | "square"–
AttributDescription
data-slot="avatar-group-count"Ciblez le compteur en CSS.
data-sizeLa taille résolue.
data-shapeLa forme résolue.

getInitials(name, max = 2) retourne jusqu’à max initiales en majuscules : celle du premier mot et celle du dernier. Pour une adresse e-mail, elle utilise la partie avant le @. Elle retourne une chaîne vide lorsque le nom ne contient ni lettre, ni chiffre, ni emoji : le contenu de repli affiche alors l’icône d’utilisateur.

Utilisé dans les blocks

Des blocks qui s’appuient sur Avatar.