Avatar
Identifies a person or entity at a glance. Use it in lists, tables, comment threads, and
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
Avatar is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { Avatar } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
Identifies a person or entity at a glance. Use it in lists, tables, comment threads, and account menus, anywhere a name alone reads slower than a face.
Rules
nameis required even whensrcis set. It is the image alt text and the initials fallback. An avatar with no name is unlabelled to a screen reader.- Use
xsandsminside dense rows,mdas the default,lgandxlfor profile headers only. statusis presence, not role or state. Do not repurpose it as a notification dot; use Badge for that.- Square avatars are for organisations and workspaces. Circles are for people.
Tradeoffs
Initials collide often in large directories. If your data set has many shared initials, supply images or add a secondary identifier next to the avatar.
Props
The typed contract, from Avatar.d.ts.
import * as React from "react";
/** Person or entity thumbnail. Falls back to initials when no image is supplied. */
export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
/** Full name. Required — it is the accessible label and the initials source. */
name: string;
/** Image URL. When absent, initials render instead. */
src?: string;
/** @default "md" */
size?: "xs" | "sm" | "md" | "lg" | "xl";
/** @default "circle" */
shape?: "circle" | "square";
/** Presence indicator in the lower-right corner. */
status?: "online" | "busy" | "away" | "offline";
}
export declare function Avatar(props: AvatarProps): React.JSX.Element;
Tokens it reads
Every custom property Avatar.jsx resolves, read from the source: 16 semantic, 1 primitive, 1 undeclared. Component tier first, because that is the one to reach for.
- semantic Shared roles. Override one and everything using that role moves with it.
- primitive Raw values. A component reading one directly is a deliberate exception.
- none Referenced by the source but declared by no tier, so it resolves to nothing.
| Token | Tier | Declared as |
|---|---|---|
--dt-border-strong | semantic | var(--dt-color-neutral-400) |
--dt-border-subtle | semantic | var(--dt-color-neutral-100) |
--dt-border-width-default | semantic | var(--dt-dim-hair) |
--dt-radius-control | semantic | var(--dt-radius-raw-8) |
--dt-radius-pill | semantic | var(--dt-radius-raw-full) |
--dt-surface-base | semantic | var(--dt-color-white) |
--dt-surface-danger | semantic | var(--dt-color-red-600) |
--dt-surface-success | semantic | var(--dt-color-green-600) |
--dt-surface-sunken | semantic | var(--dt-color-neutral-100) |
--dt-surface-warning | semantic | var(--dt-color-amber-500) |
--dt-text-heading-xs-size | semantic | var(--dt-font-size-lg) |
--dt-text-label-lg-size | semantic | var(--dt-font-size-md) |
--dt-text-label-md-family | semantic | var(--dt-font-family-secondary) |
--dt-text-label-md-size | semantic | var(--dt-font-size-sm) |
--dt-text-label-sm-size | semantic | var(--dt-font-size-xs) |
--dt-text-secondary | semantic | var(--dt-color-neutral-600) |
--dt-font-weight-medium | primitive | 500 |
--dt-border-width-thick | none | not declared |
Source
React only, no dependencies, every value a token.
import React from "react";
const SIZES = { xs: 20, sm: 24, md: 32, lg: 40, xl: 56 };
const TEXT = { xs: "var(--dt-text-label-sm-size)", sm: "var(--dt-text-label-sm-size)", md: "var(--dt-text-label-md-size)", lg: "var(--dt-text-label-lg-size)", xl: "var(--dt-text-heading-xs-size)" };
function initials(name) {
if (!name) return "";
return name.trim().split(/\s+/).slice(0, 2).map(w => w[0]).join("").toUpperCase();
}
export function Avatar({ name, src, size = "md", shape = "circle", status, style, ...rest }) {
const px = SIZES[size] || SIZES.md;
const statusColor = { online: "var(--dt-surface-success)", busy: "var(--dt-surface-danger)", away: "var(--dt-surface-warning)", offline: "var(--dt-border-strong)" }[status];
return (
<span style={{ position: "relative", display: "inline-flex", flex: "none", ...style }} {...rest}>
<span
aria-label={src ? undefined : name}
role={src ? undefined : "img"}
style={{
width: px, height: px, borderRadius: shape === "circle" ? "var(--dt-radius-pill)" : "var(--dt-radius-control)",
background: "var(--dt-surface-sunken)", color: "var(--dt-text-secondary)",
display: "inline-flex", alignItems: "center", justifyContent: "center", overflow: "hidden",
fontFamily: "var(--dt-text-label-md-family)", fontSize: TEXT[size], fontWeight: "var(--dt-font-weight-medium)",
border: "var(--dt-border-width-default) solid var(--dt-border-subtle)", userSelect: "none",
}}
>
{src ? <img src={src} alt={name || ""} style={{ width: "100%", height: "100%", objectFit: "cover" }} /> : initials(name)}
</span>
{status && (
<span aria-label={status} role="img" style={{
position: "absolute", right: -1, bottom: -1, width: Math.max(6, px * 0.28), height: Math.max(6, px * 0.28),
borderRadius: "var(--dt-radius-pill)", background: statusColor,
border: "var(--dt-border-width-thick, 2px) solid var(--dt-surface-base)",
}} />
)}
</span>
);
}
In context
Avatar also appears on the display reference cards, alongside the rest of its family.