Skip to content

Avatar

An image with a fallback that stands in while it loads, and stays if it never does.

View source View as Markdown

Avatar renders a profile picture and holds a fallback behind it until the image has loaded. Useful wherever the picture might be missing, slow or broken, and initials should stand in.

RSimage
CJbroken src
MWno image
AAAAAAAAAA
App.vue
<template>
  <div class="flex flex-col items-center gap-8">
    <div class="flex items-start gap-4">
      <div
        v-for="person in people"
        :key="person.initials"
        class="flex flex-col items-center gap-2"
      >
        <Avatar.Root :class="[avatar, sizes[2]!.box, solid]">
          <Avatar.Image
            v-if="person.src"
            :src="person.src"
            :alt="person.alt"
            class="size-full"
          />
          <Avatar.Fallback :class="initials">
            {{ person.initials }}
          </Avatar.Fallback>
        </Avatar.Root>

        <span class="type-component-2xs text-surface-muted">
          {{ person.label }}
        </span>
      </div>
    </div>

    <div class="flex items-center gap-4">
      <Avatar.Root
        v-for="size in sizes"
        :key="size.name"
        :class="[avatar, size.box, translucent]"
      >
        <Avatar.Fallback :class="initials">AA</Avatar.Fallback>
      </Avatar.Root>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Avatar } from '@maas/mirror/vue'

const people = [
  { initials: 'RS', alt: 'Robin Vey', src: '/robin.jpg', label: 'image' },
  { initials: 'CJ', alt: '', src: '/missing.png', label: 'broken src' },
  { initials: 'MW', alt: '', src: '', label: 'no image' },
]

const sizes = [
  { name: 'xs', box: 'size-8 rounded-component-sm type-component-xs' },
  { name: 'sm', box: 'size-10 rounded-component-md type-component-md' },
  { name: 'md', box: 'size-12 rounded-component-lg type-component-xl' },
  { name: 'lg', box: 'size-14 rounded-component-xl type-component-3xl' },
  { name: 'xl', box: 'size-16 rounded-component-2xl type-component-5xl' },
]

const avatar = 'flex items-center justify-center overflow-hidden'

const solid = 'bg-primary-solid text-primary-on-solid'
const translucent = 'bg-primary-subtle text-primary-on-subtle'

const initials = 'font-medium tracking-[0.02em] leading-none'
</script>

The second avatar fails to load, and the third has no image at all.

Usage guidelines

  • Size Avatar.Root rather than Avatar.Image, because nothing loads on the server: a server-rendered avatar is the fallback, and the image only arrives after hydration.
  • The server-rendered markup holds no image, so the alt text is not in the HTML a crawler reads. Where that matters, name the surrounding link or heading instead of relying on the avatar.
  • Put the name on the image’s alt, or leave alt empty and name the surrounding link, so the initials are never read out twice.

Anatomy

Assemble the root, an image and a fallback.

NameRequiredDescription
Avatar.Root true Renders a <span>. Owns the load status.
Avatar.ImageRenders an <img>, and only once the image has loaded.
Avatar.FallbackRenders a <span> while the image has not loaded.
<script setup lang="ts">
import { Avatar } from '@maas/mirror/vue'
</script>

<template>
  <Avatar.Root :options="{ delay: 600 }">
    <Avatar.Image src="/robin.jpg" alt="Robin Vey" />
    <Avatar.Fallback>RS</Avatar.Fallback>
  </Avatar.Root>
</template>

Examples

The load status

Avatar.Root owns a single piece of state, and these are the values it takes.

StatusReached when
idleNo Avatar.Image, an empty src, or the component is rendering on the server.
loadingAvatar.Image has started fetching a non-empty src in the browser.
loadedThe fetch resolved.
errorThe fetch failed.

Avatar.Image fetches through a detached Image() and renders the element only once the status is loaded, so a broken image icon never flashes.

<template>
  <Avatar.Root v-slot="{ status }">
    <Avatar.Image src="/robin.jpg" alt="Robin Vey" />
    <Avatar.Fallback>
      <span v-if="status === 'loading'" class="spinner" />
      <span v-else>RS</span>
    </Avatar.Fallback>
  </Avatar.Root>
</template>

Changing src, referrerPolicy or crossOrigin restarts the load, and only the most recent request may write the status.

When the fallback shows

Avatar.Fallback renders while the status is anything other than loaded, and with the default options.delay of 0 that means from the first paint.

<template>
  <Avatar.Root>
    <Avatar.Image src="/robin.jpg" alt="Robin Vey" />
    <Avatar.Fallback>RS</Avatar.Fallback>
  </Avatar.Root>
</template>

If the fallback is heavier than the image it stands in for, set options.delay so a fast connection does not flash it.

<template>
  <Avatar.Root :options="{ delay: 600 }">
    <Avatar.Image src="/robin.jpg" alt="Robin Vey" />
    <Avatar.Fallback>RS</Avatar.Fallback>
  </Avatar.Root>
</template>

The timer starts when the fallback mounts and re-arms whenever a new load starts, so a second src gets the same grace period as the first. It runs regardless of what the image is doing, so a delay holds back the error fallback as well as the loading one, and leaves the fallback out of the server-rendered markup. Changing options.delay after mount restarts it.

Avatar.Image is optional, and without it the status stays idle, so a user with no picture renders the fallback and nothing else.

<template>
  <Avatar.Root>
    <Avatar.Fallback>MW</Avatar.Fallback>
  </Avatar.Root>
</template>

Fading the image in

The image mounts once it has loaded, on top of a fallback that is already there, so options.transition names a Vue transition and the timing lives in CSS under that name. data-state flips between open and closed alongside it.

<template>
  <Avatar.Root :options="{ transition: 'avatar-image' }">
    <Avatar.Image src="/robin.jpg" alt="Robin Vey" />
    <Avatar.Fallback>RS</Avatar.Fallback>
  </Avatar.Root>
</template>

<style>
.avatar-image-enter-active {
  transition: opacity 200ms ease;
}

.avatar-image-enter-from {
  opacity: 0;
}
</style>

Stack the image over the fallback rather than putting them side by side, or the fallback moves out from under the fade.

Reacting to the load

statusChange fires on every transition, the initial one included, so use it to log a broken asset or swap in a different source.

<template>
  <Avatar.Root>
    <Avatar.Image :src="src" alt="Robin Vey" @status-change="onStatus" />
    <Avatar.Fallback>RS</Avatar.Fallback>
  </Avatar.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { AvatarStatus } from '@maas/mirror/vue'

const src = ref('https://cdn.example.com/robin.jpg')

function onStatus(status: AvatarStatus) {
  if (status === 'error') {
    src.value = '/robin.jpg'
  }
}
</script>

Cross-origin images

referrerPolicy and crossOrigin are set on the detached loader as well as the rendered <img>, so the request that decides the status is the one behind the element.

<template>
  <Avatar.Root>
    <Avatar.Image
      src="https://cdn.example.com/robin.jpg"
      alt="Robin Vey"
      referrer-policy="no-referrer"
      cross-origin="anonymous"
    />
    <Avatar.Fallback>RS</Avatar.Fallback>
  </Avatar.Root>
</template>

API reference

Module. A bundled options object, and useMirrorAvatar(id) as the programmatic API.

Avatar.Root

Renders a <span>.

Props

PropTypeDefault
id
stringgenerated
options
AvatarOptions{}

Slot props

PropType
status
'idle' | 'loading' | 'loaded' | 'error'

Avatar.Image

Renders an <img> while the status is loaded, and nothing otherwise.

Props

PropTypeDefault
src
stringnone
alt
string''
referrerPolicy
ReferrerPolicyundefined
crossOrigin
'' | 'anonymous' | 'use-credentials'undefined

Slot props

An <img> takes no children, so the slot is only reached through as or asChild.

PropType
status
'idle' | 'loading' | 'loaded' | 'error'

Emits

EmitPayload
statusChange
'idle' | 'loading' | 'loaded' | 'error'

Avatar.Fallback

Renders a <span> while the status is anything other than loaded. The wait before it renders is options.delay on the root.

Slot props

PropType
status
'idle' | 'loading' | 'loaded' | 'error'

Composable

useMirrorAvatar(id) reaches an avatar from anywhere in the app.

KeyType
status
ComputedRef<AvatarStatus>
setStatus
(next: AvatarStatus) => void

Data attributes

PartAttributeValue
all
data-status
idle | loading | loaded | error
all
data-scope
root | image | fallback
Avatar.Image
data-state
open | closed

CSS variables

PartVariableDefault
Avatar.Root
--mirror-avatar-user-select
none
Avatar.Image
--mirror-avatar-image-object-fit
cover

Errors

Code
missing_avatar_context

Accessibility

There are no roles, no keyboard behaviour and no focus to manage. The image has its own alt, and the fallback is plain text.