Person
People show up everywhere in the portal — rosters, team lists, assignees, comment threads. This section covers the three building blocks: the DS Avatar (a circular identity chip with an image or initials fallback), AvatarGroup (a stacked, overflow-aware cluster of avatars), and the STD PersonCard — our own wrapper that composes an Avatar with a name, secondary line, labelled meta rows and badges. Flip the theme in the top bar to confirm every part re-skins via the semantic tokens; the avatar initials background, group overlap ring and card surface all resolve from CSS vars.
- Avatar
- @savethemarshalldogs/design-system — the atom. One person, one chip. Image when src is given, otherwise initials derived from name. Five sizes.
- AvatarGroup
- @savethemarshalldogs/design-system — a compact stack of Avatars with an overlap ring and a +N overflow chip. For showing many people in a small space (team membership, attendees).
- PersonCard
- @/components/ds (STD wrapper, NOT shipped by the DS) — a full person tile: avatar + name + secondary + meta rows + badges + optional aside, optionally rendered as a link.
- which to use
- Avatar for a bare identity chip, AvatarGroup when several people collapse into one control, PersonCard when a person needs a labelled, scannable record (with contact rows / status).
Avatar — sizes
Five fixed sizes (xs · sm · md · lg · xl), each a token-driven square that renders as a circle. Size scales the chip and its type together; it never changes color or shape. md is the default. Use the smallest size that stays legible in its context.
- size="xs"
- --space-5 (20px) — dense rows, inline mentions, AvatarGroup stacks.
- size="sm"
- --space-6 (24px) — list rows, compact tables.
- size="md"
- --space-8 (32px) — the default; cards and standard rows.
- size="lg"
- --space-9 (40px) — profile headers, prominent person tiles.
- size="xl"
- --space-9 + --space-3 (≈52px) — page-level identity / hero blocks.
Avatar — image & initials fallback
Pass src and the avatar renders that image (object-fit: cover). Omit src — or when the image fails — and it falls back to initials derived from name (the DS initials() helper). Always pass name even with an image: it supplies the initials fallback and the accessible label.
- src
- Image URL. When present, renders <img class=avatar__img> with object-fit: cover over the chip. Omit for an initials-only avatar.
- name
- Always pass it. Drives the initials fallback (via the DS initials() helper) and the accessible label, even when an image is shown.
- fallback
- No src (or an empty name) → initials on the neutral chip. An empty name renders an empty chip — pass a real name in production.
Avatar — accent, ring & square
Three boolean modifiers, all optional and combinable. accent tints the initials chip with the accent-soft fill (use for the current user or a highlighted person). ring adds an inset hairline so an avatar stays separated on a busy background. square swaps the pill radius for the md radius (logos / org marks rather than people).
- accent
- boolean. Tints an initials avatar with --color-accent-soft / --color-accent. Reserve it to mark one person (e.g. the signed-in user). No effect on an image avatar's fill.
- ring
- boolean. Adds an inset hairline ring (--color-surface) so the avatar reads as separated on photos / colored panels and in AvatarGroup stacks.
- square
- boolean. Switches from the pill radius to --radius-md. Use for org/logo marks, not for people.
AvatarGroup — overflow +N
Stacks Avatars with a slight overlap and a surface-colored ring between them, then collapses everything past max into a +N chip. Use it where several people must read as one unit in a tight space — team membership, event attendees, an assignee list. Keep the children small (xs / sm) so the overlap stays tidy.
- max
- Caps how many avatars render; the remainder fold into a single +N overflow chip. Omit max to show every child.
- total
- Overrides the +N count. Use it when you only render a few avatars but want the chip to reflect a larger real total (e.g. render 4, show +96).
- children
- Plain <Avatar> elements — keep them one small size (sm/xs). The group applies the negative margin + surface ring between them.
- labelling
- The +N chip is decorative; if the group is the only thing naming a set, add an accessible label/heading nearby so it's not just a visual count.
PersonCard — the STD wrapper
PersonCard is OURS (@/components/ds), not a DS export — it composes the DS Avatar + Text on a portal surface and adds the slots our app needs: a name, a secondary line, labelled meta rows, a badge cluster and a right-aligned aside. It is presentational and safe in a server or client tree. Use it whenever a person needs a scannable record rather than a bare chip.
Foster coordinator · Brooklyn
Transport volunteer
- name (required)
- The person's name — heading text AND the Avatar initials fallback (when name is a plain string). Pass nameText alongside if name is a node.
- secondary
- The line under the name — role, team, or headline. Rendered in DS Text size=small muted; truncates.
- avatarSrc / avatarSize / avatarAccent
- Forwarded to the DS Avatar. avatarSrc shows an image (initials otherwise); avatarSize defaults to md; avatarAccent tints the initials chip.
PersonCard — meta rows
Pass an array of { label, value } meta rows to turn the card into a labelled contact record. Each row is a small uppercase label paired with a value that truncates on overflow. Use it for email / phone / team / location — anything a reader scans down a column.
Foster coordinator
- ada@savethedogs.io
- Phone
- (555) 010-2840
- Team
- Brooklyn fosters
Transport volunteer
- grace@savethedogs.io
- Region
- Hudson Valley
- meta
- PersonMetaRow[] — each { label, value, key? }. label is a small uppercase eyebrow; value truncates. Pass key when label is not a plain string.
- value can be a node
- Drop an <a> (mailto:/tel:) or a chip into value, not just text — it stays inside the truncating cell.
PersonCard — badges & aside
The badges slot takes a wrapping cluster of DS Badges / role pills (status, role, affinity flags). The aside slot is right-aligned for an action, a status chip or a chevron. Meaning is never carried by color alone — every badge keeps a text label.
Lead organizer · NYC megaregion
- katherine@savethedogs.io
Pending volunteer
- Joined
- Jun 2026
- badges
- Any node — typically one or more DS <Badge> / role pills. Rendered as a wrapping gap-1.5 cluster under the meta rows.
- aside
- Right-aligned slot (margin-left:auto, vertically centered). Use for a status chip, a single action, or a chevron — keep it to one or two items.
- never color alone
- Every badge keeps a text label (Verified, Needs review…) — tone reinforces meaning, it does not replace the word.
PersonCard — as a link
Pass href and the whole card becomes a focusable <a> with a hover border + surface shift and a visible focus ring — the canonical way to link a roster row to a profile. Without href it renders a plain, non-interactive <div>. The card is keyboard-reachable and the hit target is the entire surface.
- href
- When set, the card renders as <a href>: a hover border/surface shift + a focus-visible accent ring, keyboard-focusable, whole-surface hit target. Omit it for a static <div>.
- anchorProps / divProps
- Escape hatch for native attributes on the underlying <a> (when href set) or <div> (when not) — e.g. target, rel, onClick. className is owned by the component.
- one action per card
- When the whole card is a link, don't nest other links/buttons inside it — keep the aside non-interactive (a chevron, a status) so there's a single, predictable target.