HextaUI

Avatar

Fotos de usuario con iniciales como alternativa, insignias de estado y grupos apilados que se colapsan en un contador.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

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

Tamaños y formas

Cinco tamaños, como círculos o cuadrados. Las iniciales y el icono de usuario escalan con la caja, y las esquinas cuadradas se reducen con el tamaño. Un <AvatarFallback /> vacío muestra el icono de usuario.

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>
  )
}

Cargando

Las iniciales se muestran mientras carga la foto, y luego la foto aparece con un fundido sobre ellas. Una foto rota conserva el fallback. Pasa delay para esperar antes de mostrar las iniciales, de modo que las fotos rápidas nunca las muestren por un instante.

"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>
  )
}

Iniciales

getInitials() elige la primera y la última inicial. Maneja direcciones de correo, emoji, nombres CJK y RTL, marcas combinadas y nombres sin ninguna letra.

"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>
  )
}

Estado

<AvatarBadge /> se sitúa en el borde en cualquier tamaño y forma. Define status para un punto de color con una etiqueta accesible, o pasa un icono. Cambiar el estado reproduce un solo pulso.

"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>
  )
}

Group

<AvatarGroup /> superpone sus avatares y define su tamaño y forma. max colapsa el resto en un contador.

"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>
  )
}

Grupo con enlaces

Renderiza los avatares como enlaces con render y dale a cada uno un aria-label. Un avatar con foco se eleva por encima de sus vecinos para que el anillo nunca se corte. Añade <AvatarGroupCount /> tú mismo cuando el total provenga de tus datos.

"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>
  )
}

Diseño

Los avatares nunca se encogen en filas apretadas. Una clase de tamaño como size-20 escala con ella las iniciales y la insignia, y las iniciales largas nunca se desbordan.

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 derecha a izquierda

La insignia permanece en la esquina final, que es la izquierda en RTL, y los grupos se superponen desde la derecha.

"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>
  )
}

Los avatares no son enfocables por sí solos. Renderizados como enlace o botón, reciben las teclas habituales.

KeyAcción
TabMueve el foco al siguiente avatar con enlace.
EnterSigue al enlace con foco.
  • Usa alt="" cuando el nombre de la persona ya está junto al avatar, y su nombre como texto alternativo cuando no lo está.
  • Las insignias con status se anuncian como “Online”, “Away”, “Busy” u “Offline”. Offline se dibuja como un anillo, así que el estado nunca depende solo del color.
  • Los grupos tienen role="group". El contador se lee como “3 more”, no “+3”.
  • Con movimiento reducido, las fotos aparecen sin fundido y los cambios de estado no pulsan.

Construido sobre el avatar de Base UI. Cada parte acepta los atributos del elemento que renderiza. Los estilos se exportan como avatarVariants y avatarBadgeVariants.

PropTipoPredeterminado
sizeSe hereda del grupo si se omite.
"xs" | "sm" | "default" | "lg" | "xl""default"
shapeSe hereda del grupo si se omite.
"circle" | "square""circle"
render
ReactElement | (props, state) => ReactElement<span>
AtributoDescripción
data-slot="avatar"Selecciona los avatares en CSS.
data-sizeEl tamaño resuelto.
data-shapeLa forma resuelta.
--avatar-radiusEl radio de las esquinas, compartido por todas las capas.
PropTipoPredeterminado
src
string–
alt
string–
onLoadingStatusChange
(status: "idle" | "loading" | "loaded" | "error") => void–
keepMountedCarga la imagen en su sitio en lugar de precargarla, para loading="lazy" o next/image.
booleanfalse
render
ReactElement | (props, state) => ReactElement<img>
AtributoDescripción
data-slot="avatar-image"Selecciona las imágenes en CSS.
data-loadingPresente mientras la imagen carga.
data-errorPresente cuando la imagen no se pudo cargar.
data-starting-stylePresente mientras la imagen aparece con un fundido.
data-ending-stylePresente mientras la imagen desaparece con un fundido.
PropTipoPredeterminado
childrenVacío o con solo espacios muestra el icono de usuario.
ReactNode<IconUser />
delayMilisegundos de espera antes de mostrarlo.
number0
render
ReactElement | (props, state) => ReactElement<span>
AtributoDescripción
data-slot="avatar-fallback"Selecciona los fallbacks en CSS.
data-readyfalse hasta que pasa el retraso.
PropTipoPredeterminado
statusColorea el punto y lo etiqueta para las tecnologías de asistencia. Sin él, la insignia usa el color primario.
"online" | "away" | "busy" | "offline"–
childrenUn icono dentro de la insignia. Oculto en los tamaños xs y sm.
ReactNode–
AtributoDescripción
data-slot="avatar-badge"Selecciona las insignias en CSS.
data-statusEl estado actual.
data-slot="avatar-badge-pulse"El pulso que se reproduce tras un cambio de estado.
PropTipoPredeterminado
size
"xs" | "sm" | "default" | "lg" | "xl""default"
shape
"circle" | "square""circle"
maxCuántos elementos mostrar, incluido el contador. Los valores menores que 2 se suben a 2.
number–
AtributoDescripción
data-slot="avatar-group"Apunta a los grupos en CSS.
data-sizeEl tamaño del grupo.
PropTipoPredeterminado
countSe muestra como +3, o 99+ por encima de 99.
number–
childrenReemplaza el contador, por ejemplo con un icono.
ReactNode–
sizeSe hereda del grupo si se omite.
"xs" | "sm" | "default" | "lg" | "xl"–
shapeSe hereda del grupo si se omite.
"circle" | "square"–
AtributoDescripción
data-slot="avatar-group-count"Apunta al contador en CSS.
data-sizeEl tamaño resuelto.
data-shapeLa forma resuelta.

getInitials(name, max = 2) devuelve hasta max iniciales en mayúsculas: la de la primera palabra y la de la última. Para una dirección de correo usa la parte anterior a @. Devuelve una cadena vacía cuando el nombre no tiene letras, números ni emoji, de modo que el fallback muestra el icono de usuario.

Usado en bloques

Bloques que se construyen sobre Avatar.