Skip to content

Avatar

A person or an entity, as a picture. Reach for it wherever an account appears: beside the name in a header, down the left of a list of comments, large at the top of a profile, in a stack of the people on a document. It is the picture plus the two things to draw when there is no picture, which is the part an application otherwise writes again at every call site.

The fallback chain is the whole component. A src draws the picture; no src and a name draws the initials derived from that name; neither draws a generic glyph. All three stand on the theme's placeholder token, so a row of them does not change colour as the data fills in.

tsx
/** Who the list is of, and whether a picture was ever uploaded. */
const PEOPLE = [
  { name: 'Ada Lovelace', handle: '@ada', picture: 'ada.jpg' },
  { name: 'Grace Hopper', handle: '@grace', picture: 'grace.jpg' },
  { name: 'Katherine Johnson', handle: '@katherine', picture: '' },
  { name: '', handle: 'not signed in', picture: '' }
] as const;

/**
 * A list of people, and the three grounds an avatar can stand on.
 *
 * Every row draws the same component with the same props. What differs
 * is only what the data has: Ada and Grace have a picture, Katherine
 * has a name and no picture, so her initials are derived from it, and
 * the last row has neither, so it gets the generic glyph. Turning
 * "Show pictures" off empties every `src`, which walks the first two
 * rows down the chain to their initials without anything else in the
 * tree changing.
 *
 * Note which avatars are announced. The ones in the list carry
 * `label=""`, which declares them decorative: the name is written
 * beside each one, and a reader who heard both would hear the person
 * twice. The four at the bottom stand alone, so each is an `image`
 * named by its person, and the last of those is silent again because
 * it has no person to name.
 *
 * Nothing here names a colour. The disc under a picture that has not
 * arrived, the ground behind a pair of initials and the ground behind
 * the glyph are all the theme's `placeholder`, which is the token that
 * exists for exactly this: something standing in for content that is
 * not there.
 */
export function People(_inputs: Inputs<{}>, _ctx: ComponentContext) {
  const pictures = internalState(true);

  return (
    <column gap={16} padding={20} width={percent(100)} height={percent(100)}>
      <row gap={8} y="center">
        <Chip
          label="Show pictures"
          variant="outlined"
          size="small"
          selected={pictures}
          onPress={next => (pictures.value = next)}
        />
        <text text="Turn it off to walk every row down the fallback chain." textStyle="bodySmall" color="textMuted" />
      </row>
      <column gap={10}>
        {PEOPLE.map(person => (
          <row key={person.handle} gap={10} y="center">
            <Avatar
              src={computed(() => (pictures.value ? person.picture : ''))}
              name={person.name}
              size="medium"
              label=""
            />
            <column gap={2}>
              <text text={person.name === '' ? 'Guest' : person.name} textStyle="body" />
              <text text={person.handle} textStyle="bodySmall" color="textMuted" />
            </column>
          </row>
        ))}
      </column>
      <row gap={12} y="center" flexWrap="wrap" rowGap={12}>
        <Avatar src={computed(() => (pictures.value ? 'ada.jpg' : ''))} name="Ada Lovelace" size="small" />
        <Avatar name="Grace Hopper" size="medium" />
        <Avatar name="Katherine Johnson" initials="KJ" size="large" shape="square" />
        <Avatar size={72} />
        <text
          text="Standing alone, each is announced by its person; the last has none, so it says nothing."
          textStyle="bodySmall"
          color="textMuted"
          flexShrink={1}
        />
      </row>
    </column>
  );
}

Turn "Show pictures" off and watch the chain. Every row is the same component with the same props; what differs is only what the data has. The avatars in the list are decorative, because the name is written beside each one, and the four below stand alone and are announced.

Props

PropTypeDefaultWhat it does
srcstring or string[]noneThe picture, or several urls for the same face tried in order. Absent or empty falls through to the initials.
namestringnoneThe person or entity. The initials come from it, and so does the accessible name
initialsstringderivedThe initials to draw, when the derived ones are wrong. Empty is the same as none
iconstringa personSVG path data for the last-resort glyph, on the usual 24 grid
sizeAvatarSize or number'medium'A named step, or the side in logical pixels
shapeAvatarShape'circle'circle, or a rounded square
labelstringnameThe accessible name. '' declares the avatar decorative
refUiNodeRefnoneReceives the node that is the avatar, for anchoring a menu to it

Every prop takes a plain value or an Observable of one, and the layout props on the library page apply here too. There is no colour prop, here or anywhere else in the library.

tsx
<Avatar src={account.avatar} name={account.name} />

src takes a list as well as a string, because that is the shape an account usually arrives in: Audius returns several sizes of the same face, and Segue's header held exactly that list with an emptiness check written around it by hand. A list is tried in order, as Image tries one, and a list of nothing but blanks counts as no picture rather than as a fetch that can only fail.

That single line is the whole of it. src empty or absent moves to the initials, and no initials moves to the glyph. Empty counts as absent on purpose: the shape an application actually holds is a string that is sometimes '', and making the caller turn that into undefined is the guard this component exists to absorb.

What the chain cannot include is a failed load, and the reason is worth stating plainly rather than leaving as a surprise. Image is told by its modifier when a source fails, but it keeps that to itself and spends it on the placeholder tint; ImageProps has no onError, no onState and no status output of any kind. So nothing outside an Image, including this component, can learn that a url did not resolve. Avatar therefore falls back on an absent or empty src only, which is a question it can answer, rather than pretending to a recovery it has no way to trigger.

A url that 404s leaves the avatar showing the placeholder disc, which is the same ground the initials would have stood on, so a broken picture degrades to a blank face rather than to a hole. If an application needs more than that today, it has to know the url is bad before it passes it.

Initials

NameInitialsWhy
Ada LovelaceALThe given name and the family name
AdaAOne name, one letter
Ada B. LovelaceALThe first part and the last, because a middle one is neither
Ålesund KommuneÅKOne grapheme, not one code unit
村上 春樹村春A script with no case is left alone
`` (empty)noneNothing to derive, so the glyph is next

The slice is by grapheme and never by code unit. A name beginning with an emoji, a flag, a Devanagari cluster or a letter carrying combining marks is one visible character made of several code units, and name[0] cuts it in half: half a surrogate pair draws as a replacement character, and a base letter stripped of its marks is the wrong letter. The runtime already answers this question for the caret, so that Backspace deletes a flag rather than half of one, and Avatar asks the same function.

Case is raised with toLocaleUpperCase, which does nothing in the scripts that have no case and the right thing in the ones that do. Pass initials for a name the rule cannot read the way a reader would: a company that goes by three letters, a handle that is not a name, a person whose family name comes first.

Size and shape

StepSideWhere it fits
small24 pxBeside a word, in a header or a byline
medium40 pxA row in a list of comments or of people
large64 pxA card, or a person's own entry

Three steps rather than five, because size also takes a number: a named step earns its name by being reached for repeatedly, and these are the three the applications reach for. A profile header at 128 is a number at the call site, where the reason for it is visible.

Everything inside scales with the side rather than coming from a table, so a number is as well served as a step. The initials are 40% of the side, which keeps two capitals inside the disc at every size; the glyph is 60%, which is what a shoulders-up figure needs to look centred rather than small; and a square avatar's corners are a sixth of the side, because one fixed radius would be a blob at 24 and a sharp corner at 128. That radius does not name a step of the shape scale on purpose: the shared scale has no name for a radius that is a function of a box, and inventing one for a library's sake would put the library's taste in everyone's vocabulary.

Size and shape are both followed rather than read once, so an avatar that grows when a page opens under it does not need a new key, which matters because a new key is exactly what a shared-element morph between the two must not have.

Semantics

WhatValue
Roleimage, on the disc itself, when there is a name to announce
Namelabel if it was given, else name
Statesnone: an avatar is a picture, not a control

The decision to make deliberately is which of the two cases you are in.

An avatar beside the name it depicts is a picture of a word that is already on the screen. Announcing it makes a reader hear "Ada Lovelace, image, Ada Lovelace", so it should be silent, and label="" declares that: no role, no name, nothing in the semantics tree at all. Still drawn, because decorative means unannounced and not absent.

An avatar that stands alone, in a grid of faces or a stack of authors, is the only thing naming the person and must announce them. That is the default: with no label, the accessible name is name.

tsx
// In a list, beside the name: silent.
<row><Avatar src={person.avatar} name={person.name} label="" /><text text={person.name} /></row>

// On its own: announced.
<Avatar src={person.avatar} name={person.name} />

An avatar with neither a label nor a name is decorative too, because it has nothing to say. The picture inside never has a record of its own: the avatar carries the role and the name, and a second record inside it would announce the person twice.

Colours

None of them are props. The ground is placeholder, the palette token that exists for something standing in for content that has not arrived. Not border, which would tie a filled disc to the colour of a rule, and not controlBackgroundPressed, which would move every avatar on the screen when a theme adjusted how a button looks while held. The initials are text and the glyph is textMuted, and that difference is deliberate: initials are information and have to be read, while the glyph says only "no picture" and should not shout it.

Restyling is a theme provider around the avatar, the mechanism themes and the environment describes.

What this page was checked against

Avatar.spec.ts mounts the component with gesso-testing and asserts each link of the chain and that only one of them is in the tree at a time, that an arriving src swaps the layer without rebuilding the node that is the avatar, that explicit initials win over the derived ones, that the derivation takes the first and last parts and never cuts a grapheme, that the labelled case is an image named by the person and label="" is silent while still drawn, that the picture inside adds no second record, that each named step and a number give their own side, that a size which changes is followed, that a circle and a square differ only in radius, and that the ground is placeholder. AvatarExample.spec.ts asserts what the example above claims, by role and name.

Next

Image is the picture underneath this one, and says what a source that fails does and does not report.