Avatar
Shows a face, initials, or fallback icon at a fixed size. People in lists, comments, and headers.
Sizes: xssml (default: s)
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Person in a list:
```tsx import { Avatar } from '@/components/atoms/Avatar';
<Avatar type="image-border" size="m" src="/u/ada.jpg" alt="Ada Lovelace" status /> ```
Initials fallback:
```tsx <Avatar type="initials" size="s" initials="AL" shape="circle" /> ```
## Accesibilidad
- Image type needs meaningful `alt` (person/entity name). Initials/icon types need `aria-label` when the name is not adjacent text. - `status` is decorative presence — do not encode critical state only in the pip color.
### Correcto
- Imagen: alt describe al usuario (ej: 'Karen Ortiz') — no 'avatar' ni 'foto' - Icon default tiene stroke-dasharray para indicar visualmente que es placeholder - Status dot es decorativo — no tiene role ni aria. Comunicar status via texto adyacente - Skeleton oculta todo el contenido con display:none — screen readers no leen nada (correcto)
### Evitar
- No usar Avatar sin alt cuando tiene imagen — es obligatorio para a11y - No depender del status dot como unica indicacion de estado online — acompanar con texto - No usar initials de mas de 2 caracteres — se desborda del espacio
## Cuándo no usar
- Product/hero photography → `Image`. - Stacks of people → `AvatarGroup`.
## Criterio de uso
- Elige `image` cuando la identidad visual importe, `initials` como fallback estable y `icon` para bots o entidades sin rostro. - Usa círculo para personas y cuadrado para marcas, workspaces o entidades no humanas; mantén la decisión consistente en una misma superficie. - Muestra `status` sólo si la presencia es actual y accionable; una pip de color sin información confiable crea una falsa expectativa.
## Gotchas
- La imagen necesita `alt` significativo; initials e icon necesitan un nombre accesible cuando el contexto no lo aporta. - `skeleton` debe desaparecer cuando termina la carga y conservar el tamaño final para evitar saltos de layout.
Uso
import { Avatar } from '@/components/atoms/Avatar';
<Avatar type="image-border" size="m" src="/u/a.jpg" alt="Ada Lovelace" status />Props
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| type | select | image-border | `image`, `image-border`, `initials`, `icon` | Content strategy for the avatar face. | image-border for product UIs with photo; initials when photo missing; icon for bots. Default: image-border. |
| shape | select | circle | `circle`, `square` | Outer shape of the avatar. | circle default for people; square for brands/workspaces. Default: circle. |
| size | select | s | `xs`, `s`, `m`, `l` | Pixel scale of the avatar. | s in lists; m in headers; l for profile heroes; xs in dense tables. Default: s. |
| status | boolean | false | `true` / `false` | Shows the presence/status pip. | true only when presence is real-time and meaningful. Default: false. |
| skeleton | boolean | false | `true` / `false` | Loading placeholder surface. | true while the image URL is resolving. Default: false. |
Gotchas
- a11y
Provide alt for image type or aria-label for initials/icon so the person/entity is named.
Anatomía CSS
<div class="avatar"> <span class="avatar__icon"></span> <span class="avatar__image"></span> <span class="avatar__status"></span> </div>
| Clase | Propósito |
|---|---|
avatar | root |
avatar--circle | modifier |
avatar--icon | modifier |
avatar--image | modifier |
avatar--image-border | modifier |
avatar--initials | modifier |
avatar--l | modifier |
avatar--m | modifier |
avatar--s | modifier |
avatar--skeleton | modifier |
avatar--square | modifier |
avatar--xs | modifier |
avatar__icon | element |
avatar__image | element |
avatar__status | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | #25d366 | success |
| image | bg | none | (unparsed) |
| initials | fg | #171717 | border.focus |
| initials | border | 2px | stroke.medium |
| icon | fg | #525252 | muted.foreground |
| icon | border | 2px | stroke.medium |
| all | border | #fafafa | background |
| skeleton | border | none | (unparsed) |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
@keyframes avatar-skeleton-pulse | | |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Avatar
User representation: image, initials, or icon placeholder.
Optional status dot (online indicator).
Types: image, image-border, initials, icon
Shapes: circle, square
Sizes: xs (24px), s (32px), m (40px), l (48px)
------------------------------------------------------------------------- */
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
background-color: #f5f5f5;
}
.avatar__image {
overflow: hidden;
}
/* ---- Sizes ---- */
.avatar--xs { width: 1.5rem; height: 1.5rem; }
.avatar--s { width: 2rem; height: 2rem; }
.avatar--m { width: 2.5rem; height: 2.5rem; }
.avatar--l { width: 3rem; height: 3rem; }
/* ---- Shapes ---- */
.avatar--circle { border-radius: 9999px; }
.avatar--square.avatar--xs { border-radius: 8px; }
.avatar--square.avatar--s { border-radius: 8px; }
.avatar--square.avatar--m { border-radius: 12px; }
.avatar--square.avatar--l { border-radius: 16px; }
/* ---- Types ---- */
/* Image (no border) */
.avatar--image {
background: none;
padding: 0;
}
/* Image with border ring */
.avatar--image-border {
padding: 0.125rem;
}
.avatar--image-border.avatar--m,
.avatar--image-border.avatar--l {
padding: 0.25rem;
}
/* Initials */
.avatar--initials {
border: 2px solid #e5e5e5;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-weight: 400;
color: #171717;
font-size: 12.8px;
line-height: 1;
}
.avatar--initials.avatar--l {
font-size: 16px;
}
/* Icon */
.avatar--icon {
border: 2px solid #e5e5e5;
color: #525252;
}
/* ---- Image element ---- */
.avatar__image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: inherit;
}
.avatar--circle .avatar__image {
border-radius: 9999px;
}
/* ---- Icon element ---- */
.avatar__icon {
display: flex;
align-items: center;
justify-content: center;
}
.avatar--xs .avatar__icon { width: 0.75rem; height: 0.75rem; }
.avatar--s .avatar__icon { width: 1rem; height: 1rem; }
.avatar--m .avatar__icon { width: 1.25rem; height: 1.25rem; }
.avatar--l .avatar__icon { width: 1.5rem; height: 1.5rem; }
.avatar__icon svg {
width: 100%;
height: 100%;
}
/* ---- Status dot ---- */
.avatar__status {
position: absolute;
background-color: #25d366;
border-radius: 9999px;
border-style: solid;
border-color: #fafafa;
}
.avatar--xs .avatar__status { width: 0.5rem; height: 0.5rem; border-width: 1px; bottom: -0.0625rem; right: -0.0625rem; }
.avatar--s .avatar__status { width: 0.5rem; height: 0.5rem; border-width: 1px; bottom: 0; right: 0; }
.avatar--m .avatar__status { width: 0.75rem; height: 0.75rem; border-width: 1.5px; bottom: 0; right: 0; }
.avatar--l .avatar__status { width: 1rem; height: 1rem; border-width: 2px; bottom: 0; right: 0; }
/* ---- Skeleton ---- */
.avatar--skeleton {
border: none;
animation: avatar-skeleton-pulse 1.5s ease-in-out infinite;
}
.avatar--skeleton * {
display: none;
}
@keyframes avatar-skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
Codigo fuente
import { type ReactNode } from 'react'; type AvatarType = 'image' | 'image-border' | 'initials' | 'icon'; type AvatarShape = 'circle' | 'square'; type AvatarSize = 'xs' | 's' | 'm' | 'l'; export type AvatarProps = { type?: AvatarType; shape?: AvatarShape; size?: AvatarSize; src?: string; alt?: string; initials?: string; icon?: ReactNode; status?: boolean; skeleton?: boolean; className?: string; }; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } const DefaultIcon = () => ( <svg width="100%" height="100%" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" strokeDasharray="2 2"> <circle cx="8" cy="6" r="3" /> <path d="M3 14a5 5 0 0110 0" /> </svg> ); export function Avatar({ type = 'image-border', shape = 'circle', size = 's', src, alt = '', initials, icon, status = false, skeleton = false, className, }: AvatarProps) { const classes = cn( 'avatar', `avatar--${type}`, `avatar--${shape}`, `avatar--${size}`, skeleton && 'avatar--skeleton', className, ); const renderContent = () => { if (skeleton) return null; if (type === 'image' || type === 'image-border') { return src ? <img className="avatar__image" src={src} alt={alt} /> : <span className="avatar__icon"><DefaultIcon /></span>; } if (type === 'initials') { return <span>{initials || '??'}</span>; } if (type === 'icon') { return <span className="avatar__icon">{icon || <DefaultIcon />}</span>; } return null; }; return ( <span className={classes}> {renderContent()} {status && !skeleton && <span className="avatar__status" />} </span> ); }
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / Avatar) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
selectoron.avatar--square.avatar--xs— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.avatar--square.avatar--s— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.avatar--square.avatar--m— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.avatar--square.avatar--l— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.avatar--image-border.avatar--m, .avatar--image-border.avatar--l— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.avatar--initials.avatar--l— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.avatar--circle .avatar__image— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.avatar--xs .avatar__icon— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)
Tras pegar: Create component → nombre Atom / Avatar → Publish.