---
name: avatar
title: Avatar
url: https://ui.frontic.com/components/ui/avatar.md
tier: ui
install: npx @frontic/ui add avatar
files:
  - components/ui/avatar/Avatar.vue
  - components/ui/avatar/AvatarBadge.vue
  - components/ui/avatar/AvatarFallback.vue
  - components/ui/avatar/AvatarGroup.vue
  - components/ui/avatar/AvatarGroupCount.vue
  - components/ui/avatar/AvatarImage.vue
  - components/ui/avatar/context.ts
  - components/ui/avatar/index.ts
dependencies:
  - "@vueuse/core"
  - class-variance-authority
  - reka-ui
---

# Avatar

A person's picture with initials or an icon as fallback and an optional status badge; a group overlaps several and shows how many more there are.

## Install

```sh
npx @frontic/ui add avatar
```

Used by: [Review Cards](https://ui.frontic.com/components/sections/review-cards.md), [Review List](https://ui.frontic.com/components/sections/review-list.md), [Reviews](https://ui.frontic.com/components/sections/reviews.md), [Team Members](https://ui.frontic.com/components/sections/team-members.md)

npm packages: `@vueuse/core`, `class-variance-authority`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage } from '@/components/ui/avatar'
</script>

<template>
  <AvatarGroup>
    <Avatar>
      <AvatarImage src="https://static.frontic.com/cdn-cgi/image/width=128,format=auto/ui/avatars/1.jpg" alt="Shop owner" />
      <AvatarFallback>FR</AvatarFallback>
      <AvatarBadge />
    </Avatar>
    <Avatar>
      <AvatarImage src="https://static.frontic.com/cdn-cgi/image/width=128,format=auto/ui/avatars/2.jpg" alt="Store manager" />
      <AvatarFallback>SM</AvatarFallback>
    </Avatar>
    <Avatar>
      <AvatarFallback>LB</AvatarFallback>
    </Avatar>
    <AvatarGroupCount>+3</AvatarGroupCount>
  </AvatarGroup>
</template>
```

## Anatomy

```vue
<AvatarGroup>
  <Avatar>
    <AvatarImage />
    <AvatarFallback />
    <AvatarBadge />
  </Avatar>
  <AvatarGroupCount />
</AvatarGroup>
```

## API reference

### Avatar

Groups the image, fallback and badge of an avatar. Renders a `<span>`.

`data-slot="avatar"` · `Avatar.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `AvatarSize`: `'md' \| 'sm' \| 'lg'` |  | The avatar's diameter; fallback and badge scale with it. Defaults to its group's size, else md. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-size` | `'md' \| 'sm' \| 'lg'` | Carries the `size` prop. |

### AvatarBadge

Shows a small status marker on the corner of the avatar. Renders a `<span>`.

`data-slot="avatar-badge"` · `AvatarBadge.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### AvatarFallback

Shows initials or an icon when the avatar image is missing or fails to load. Renders a `<span>`.

`data-slot="avatar-fallback"` · `AvatarFallback.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `delayMs` | `number` |  | Useful for delaying rendering so it only appears for those with slower connections. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### AvatarGroup

Stacks several avatars in an overlapping row. Renders a `<div>`.

`data-slot="avatar-group"` · `AvatarGroup.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `AvatarSize`: `'md' \| 'sm' \| 'lg'` |  | The size of every avatar and count in the group; one of them sets its own to differ. |

### AvatarGroupCount

Shows how many more avatars the group holds than it displays. Renders a `<div>`.

`data-slot="avatar-group-count"` · `AvatarGroupCount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `AvatarSize`: `'md' \| 'sm' \| 'lg'` |  | The count's diameter. Defaults to its group's size, else md. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-size` | `'md' \| 'sm' \| 'lg'` | Carries the `size` prop. |

### AvatarImage

Shows the picture of the avatar. Renders a `<img>`.

`data-slot="avatar-image"` · `AvatarImage.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `src` (required) | `string` |  | The image URL; the fallback shows while it loads and when it fails. |
| `referrerPolicy` | `HTMLAttributeReferrerPolicy`: `'' \| 'no-referrer' \| 'no-referrer-when-downgrade' \| 'origin' \| 'origin-when-cross-origin' \| 'same-origin' \| 'strict-origin' \| 'strict-origin-when-cross-origin' \| 'unsafe-url'` |  | The image request's referrer policy, set on the `<img>`. |
| `crossOrigin` | `'' \| 'anonymous' \| 'use-credentials'` |  | The image request's CORS mode, set on the `<img>`. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'img'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

## Exports

```ts
import { type AvatarSize } from '@/components/ui/avatar'
```

- `AvatarSize`: `'sm' \| 'md' \| 'lg'`

## Styling

| Part | Recipe |
| --- | --- |
| `group` | [`avatar-group`](https://ui.frontic.com/DESIGN.md#avatar-group) |
| `default` | [`avatar`](https://ui.frontic.com/DESIGN.md#avatar) |
| `ring` | [`avatar-ring`](https://ui.frontic.com/DESIGN.md#avatar-ring) |
| `image` | [`avatar-image`](https://ui.frontic.com/DESIGN.md#avatar-image) |
| `fallback` | [`avatar-fallback`](https://ui.frontic.com/DESIGN.md#avatar-fallback) |
| `badge` | [`avatar-badge`](https://ui.frontic.com/DESIGN.md#avatar-badge) |
| `group-count` | [`avatar-group-count`](https://ui.frontic.com/DESIGN.md#avatar-group-count) |

## Source

`components/ui/avatar/Avatar.vue`

```vue
<script lang="ts">
/** Groups the image, fallback and badge of an avatar. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { AvatarSize } from './context'
import { cva } from 'class-variance-authority'
import { AvatarRoot } from 'reka-ui'
import { computed, inject, provide } from 'vue'
import { AVATAR_GROUP, AVATAR_SIZE } from './context'

const avatar = cva('fui-avatar group/avatar relative select-none after:absolute after:inset-0 after:mix-blend-darken', {
  variants: {
    size: {
      sm: 'fui-avatar-sm',
      md: 'fui-avatar-md',
      lg: 'fui-avatar-lg',
    },
  },
  defaultVariants: { size: 'md' },
})

const props = defineProps<{
  class?: HTMLAttributes['class']
  /** The avatar's diameter; fallback and badge scale with it. Defaults to its group's size, else md. */
  size?: AvatarSize
}>()

const group = inject(AVATAR_GROUP, null)
const grouped = group !== null
const size = computed(() => props.size ?? group?.value ?? 'md')
provide(AVATAR_SIZE, size)
</script>

<template>
  <AvatarRoot data-slot="avatar" :data-size="size" :class="[avatar({ size }), grouped && 'fui-avatar-ring', props.class]">
    <slot />
  </AvatarRoot>
</template>
```

`components/ui/avatar/AvatarBadge.vue`

```vue
<script lang="ts">
/** Shows a small status marker on the corner of the avatar. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cva } from 'class-variance-authority'
import { computed, inject } from 'vue'
import { AVATAR_SIZE } from './context'

const avatarBadge = cva('fui-avatar-badge absolute inset-e-0 bottom-0 z-10 bg-blend-color select-none', {
  variants: {
    size: {
      sm: 'fui-avatar-badge-sm',
      md: 'fui-avatar-badge-md',
      lg: 'fui-avatar-badge-lg',
    },
  },
  defaultVariants: { size: 'md' },
})

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
    }
  >(),
  { as: 'span' },
)

const size = inject(
  AVATAR_SIZE,
  computed(() => 'md' as const),
)
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="avatar-badge" :class="[avatarBadge({ size }), size === 'sm' && '*:hidden', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/avatar/AvatarFallback.vue`

```vue
<script lang="ts">
/** Shows initials or an icon when the avatar image is missing or fails to load. */
export default {}
</script>

<script setup lang="ts">
import type { AvatarFallbackProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { cva } from 'class-variance-authority'
import { AvatarFallback, useForwardProps } from 'reka-ui'
import { computed, inject } from 'vue'
import { AVATAR_SIZE } from './context'

const avatarFallback = cva('fui-avatar-fallback', {
  variants: {
    size: {
      sm: 'fui-avatar-fallback-sm',
      md: 'fui-avatar-fallback-md',
      lg: 'fui-avatar-fallback-lg',
    },
  },
  defaultVariants: { size: 'md' },
})

const props = defineProps<AvatarFallbackProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const size = inject(
  AVATAR_SIZE,
  computed(() => 'md' as const),
)
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <AvatarFallback data-slot="avatar-fallback" v-bind="forwarded" :class="[avatarFallback({ size }), props.class]">
    <slot />
  </AvatarFallback>
</template>
```

`components/ui/avatar/AvatarGroup.vue`

```vue
<script lang="ts">
/** Stacks several avatars in an overlapping row. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { AvatarSize } from './context'
import { computed, provide } from 'vue'
import { AVATAR_GROUP } from './context'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
      /** The size of every avatar and count in the group; one of them sets its own to differ. */
      size?: AvatarSize
    }
  >(),
  { as: 'div' },
)

provide(
  AVATAR_GROUP,
  computed(() => props.size),
)
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="avatar-group" :class="['fui-avatar-group group/avatar-group -space-x-2', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/avatar/AvatarGroupCount.vue`

```vue
<script lang="ts">
/** Shows how many more avatars the group holds than it displays. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { AvatarSize } from './context'
import { cva } from 'class-variance-authority'
import { computed, inject } from 'vue'
import { AVATAR_GROUP } from './context'

const avatarGroupCount = cva('fui-avatar-group-count relative', {
  variants: {
    size: {
      sm: 'fui-avatar-group-count-sm',
      md: 'fui-avatar-group-count-md',
      lg: 'fui-avatar-group-count-lg',
    },
  },
  defaultVariants: { size: 'md' },
})

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
      /** The count's diameter. Defaults to its group's size, else md. */
      size?: AvatarSize
    }
  >(),
  { as: 'div' },
)

const group = inject(AVATAR_GROUP, null)
const size = computed(() => props.size ?? group?.value ?? 'md')
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="avatar-group-count" :data-size="size" :class="[avatarGroupCount({ size }), props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/avatar/AvatarImage.vue`

```vue
<script lang="ts">
/** Shows the picture of the avatar. */
export default {}
</script>

<script setup lang="ts">
import type { AvatarImageProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AvatarImage, useForwardProps } from 'reka-ui'

const props = defineProps<AvatarImageProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <AvatarImage data-slot="avatar-image" v-bind="forwarded" :class="['fui-avatar-image aspect-square object-cover', props.class]">
    <slot />
  </AvatarImage>
</template>
```

`components/ui/avatar/context.ts`

```ts
import type { ComputedRef, InjectionKey } from 'vue'

export type AvatarSize = 'sm' | 'md' | 'lg'

export const AVATAR_SIZE: InjectionKey<ComputedRef<AvatarSize>> = Symbol('avatar-size')
/** The size an AvatarGroup gives its avatars and count; provided only inside a group. */
export const AVATAR_GROUP: InjectionKey<ComputedRef<AvatarSize | undefined>> = Symbol('avatar-group')
```
