Avatar
An image with a fallback that stands in while it loads, and stays if it never does.
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.
<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.Rootrather thanAvatar.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
alttext 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 leavealtempty and name the surrounding link, so the initials are never read out twice.
Anatomy
Assemble the root, an image and a fallback.
| Name | Required | Description |
|---|---|---|
| Avatar.Root | true | Renders a <span>. Owns the load status. |
| Avatar.Image | Renders an <img>, and only once the image has loaded. | |
| Avatar.Fallback | Renders 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.
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
Slot props
Avatar.Image
Renders an <img> while the status is loaded, and nothing otherwise.
Props
Slot props
An <img> takes no children, so the slot is only reached through as or
asChild.
Emits
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
Composable
useMirrorAvatar(id) reaches an avatar from anywhere in the app.
Data attributes
CSS variables
Errors
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.