Person

PersonAvatar and PersonInfo are the standard way to render a person anywhere in the product. They guarantee a single visual language: one uppercase initial (name, falling back to email, then ?), a deterministic hash color (id, falling back to email, then name), and photos rendered with referrerPolicy="no-referrer".

Usage

The canonical person row: a lg avatar with the name and email stacked next to it.

E
Egor Spirin
C
Charles Peykov
L
Leslie-Alexandre Denis
import { PersonAvatar, PersonInfo } from '@jamie/ui/person'
 
const people = [
  {
    id: 'u1',
    name: 'Egor Spirin',
    email: 'egor@meetjamie.ai',
    imageUrl: 'https://github.com/espj.png'
  },
  { id: 'u2', name: 'Charles Peykov', email: 'charles@meetjamie.ai', imageUrl: null },
  { id: 'u3', name: 'Leslie-Alexandre Denis', email: 'leslie@meetjamie.ai', imageUrl: null }
]
 
export function PersonDemo() {
  return (
    <div className="flex w-72 flex-col">
      {people.map((person) => (
        <div key={person.id} className="flex items-center gap-2 rounded-sm px-1 py-1.5">
          <PersonAvatar
            size="lg"
            name={person.name}
            email={person.email}
            imageUrl={person.imageUrl}
            colorKey={person.id}
          />
          <PersonInfo name={person.name} email={person.email} />
        </div>
      ))}
    </div>
  )
}

Sizes

PersonAvatar comes in six sizes: xs, sm, md, lg, xl, and 2xl. Use xs/sm for inline chips and stacks, md for menu rows, lg for list rows, and xl/2xl for tables and profile cards.

EEEEEE
import { PersonAvatar } from '@jamie/ui/person'
 
const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const
 
export function PersonAvatarSizesDemo() {
  return (
    <div className="flex items-center gap-3">
      {sizes.map((size) => (
        <PersonAvatar
          key={size}
          size={size}
          name="Egor Spirin"
          email="egor@meetjamie.ai"
          colorKey="u1"
        />
      ))}
    </div>
  )
}

Fallbacks

When no image is available, the avatar shows the person's initial on a color derived from colorKey (pass a stable person or user id). Without a colorKey, the color falls back to the email, then the name, so the same person keeps the same color across surfaces. With no identity at all, a neutral ? is shown.

ECN?
import { PersonAvatar } from '@jamie/ui/person'
 
export function PersonAvatarFallbackDemo() {
  return (
    <div className="flex items-center gap-3">
      <PersonAvatar size="xl" name="Egor Spirin" imageUrl="https://github.com/espj.png" />
      <PersonAvatar size="xl" name="Charles Peykov" colorKey="u2" />
      <PersonAvatar size="xl" email="no-name@meetjamie.ai" />
      <PersonAvatar size="xl" />
    </div>
  )
}

Group

Use PersonAvatarGroup to stack multiple avatars with size-aware overlap and rings, and PersonAvatarGroupCount for overflow.

EML
+3
EML
+3
import { PersonAvatar, PersonAvatarGroup, PersonAvatarGroupCount } from '@jamie/ui/person'
 
const people = [
  { id: 'u1', name: 'Egor Spirin', imageUrl: 'https://github.com/espj.png' },
  { id: 'u2', name: 'Matt Milne', imageUrl: null },
  { id: 'u3', name: 'Leslie-Alexandre Denis', imageUrl: null }
]
 
export function PersonAvatarGroupDemo() {
  return (
    <div className="flex items-center gap-8">
      <PersonAvatarGroup size="sm">
        {people.map((person) => (
          <PersonAvatar
            key={person.id}
            size="sm"
            name={person.name}
            imageUrl={person.imageUrl}
            colorKey={person.id}
          />
        ))}
        <PersonAvatarGroupCount size="sm">+3</PersonAvatarGroupCount>
      </PersonAvatarGroup>
      <PersonAvatarGroup size="md">
        {people.map((person) => (
          <PersonAvatar
            key={person.id}
            size="md"
            name={person.name}
            imageUrl={person.imageUrl}
            colorKey={person.id}
          />
        ))}
        <PersonAvatarGroupCount size="md">+3</PersonAvatarGroupCount>
      </PersonAvatarGroup>
    </div>
  )
}