# 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.

::component-preview{name="AvatarPreview"}
```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.

::component-anatomy
---
parts:
  - name: Avatar.Root
    required: true
    description: Renders a <span>. Owns the load status.
    children:
      - name: Avatar.Image
        description: Renders an <img>, and only once the image has loaded.
      - name: Avatar.Fallback
        description: Renders a <span> while the image has not loaded.
---
::

```vue
<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.

::docs-table
---
columns:
  - label: Status
  - label: Reached when
rows:
  - items:
      - label: idle
      - label: No `Avatar.Image`, an empty `src`, or the component is rendering on the server.
        plaintext: true
  - items:
      - label: loading
      - label: '`Avatar.Image` has started fetching a non-empty `src` in the browser.'
        plaintext: true
  - items:
      - label: loaded
      - label: The fetch resolved.
        plaintext: true
  - items:
      - label: error
      - label: The fetch failed.
        plaintext: true
---
::

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

```vue
<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.

```vue
<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.

```vue
<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.

```vue
<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.

```vue
<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.

```vue
<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.

```vue
<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

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: 'The instance ID, for [`useMirrorAvatar`](#composable).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: options
        description: Bundled configuration for `delay`, in milliseconds, and `transition`.
      - label: AvatarOptions
      - label: '{}'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: status
        description: Mirrors `data-status`.
      - label: '''idle'' | ''loading'' | ''loaded'' | ''error'''
---
::

### `Avatar.Image`

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

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: src
        description: 'Required. An empty one leaves [the status](#the-load-status) at `idle`.'
      - label: string
      - label: none
        plaintext: true
  - items:
      - label: alt
        description: Leave it empty when something else already names the avatar.
      - label: string
      - label: ''''''
  - items:
      - label: referrerPolicy
        description: Set on the loader and the element.
      - label: ReferrerPolicy
      - label: undefined
  - items:
      - label: crossOrigin
        description: Set on the loader and the element.
      - label: ''''' | ''anonymous'' | ''use-credentials'''
      - label: undefined
---
::

#### Slot props

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

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: status
        description: Mirrors `data-status`.
      - label: '''idle'' | ''loading'' | ''loaded'' | ''error'''
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: statusChange
        description: The load status changes, initial transition included.
      - label: '''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`](#when-the-fallback-shows) on the root.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: status
        description: Mirrors `data-status`.
      - label: '''idle'' | ''loading'' | ''loaded'' | ''error'''
---
::

### Composable

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

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: status
        description: Mirrors `data-status`.
      - label: ComputedRef<AvatarStatus>
        escape: true
  - items:
      - label: setStatus
        description: Sets the status.
      - label: '(next: AvatarStatus) => void'
        escape: true
---
::

### Data attributes

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_avatar_context
        description: '`Avatar.Image` or `Avatar.Fallback` rendered outside `Avatar.Root`.'
---
::

## 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.
