Skip to main content
Nexus Design System
GitHub

Avatar

Avatar shows a person's profile image, with initials as the fallback. Use AvatarGroup to stack several people and AvatarStatus to show presence.

AL
'use client';

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from '@/components/nxds/components/avatar/avatar';

export default function AvatarDefault() {
  return (
    <Avatar size="md" shape="circle" ring={false}>
      <AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace" />
      <AvatarFallback>AL</AvatarFallback>
    </Avatar>
  );
}

Installation

npx nxds-cli@latest add avatar

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  AvatarStatus,
} from '@/components/nxds/components/avatar/avatar';

Composition

Avatar
├── AvatarImage
├── AvatarFallback
└── AvatarStatus
AvatarGroup
└── Avatar
    ├── AvatarImage
    ├── AvatarFallback
    └── AvatarStatus

Examples

Status variants

Show online, away, busy and offline presence dots.

ALOnlineGHAwayKJBusyMJOffline
'use client';

import {
  Avatar,
  AvatarFallback,
  AvatarStatus,
} from '@/components/nxds/components/avatar/avatar';

const STATUS_VALUES = ['online', 'away', 'busy', 'offline'] as const;

const PORTRAITS = [
  { name: 'Ada Lovelace', initials: 'AL', src: '/avatars/ada.svg' },
  { name: 'Grace Hopper', initials: 'GH', src: '/avatars/grace.svg' },
  { name: 'Katherine Johnson', initials: 'KJ', src: '/avatars/katherine.svg' },
  { name: 'Mary Jackson', initials: 'MJ', src: '/avatars/mary.svg' },
  { name: 'Dorothy Vaughan', initials: 'DV', src: '/avatars/dorothy.svg' },
];

export default function AvatarStatusVariants() {
  return (
    <div className="nx:flex nx:items-center nx:gap-4">
      {STATUS_VALUES.map((status, index) => (
        <Avatar key={status} size="lg">
          <AvatarFallback>{PORTRAITS[index]?.initials ?? 'NA'}</AvatarFallback>
          <AvatarStatus status={status} />
        </Avatar>
      ))}
    </div>
  );
}

Group with max

Collapse extra avatars into a +N tile with max.

ALOnlineGHAwayKJBusy
'use client';

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  AvatarStatus,
} from '@/components/nxds/components/avatar/avatar';

const STATUS_VALUES = ['online', 'away', 'busy', 'offline'] as const;

const PORTRAITS = [
  { name: 'Ada Lovelace', initials: 'AL', src: '/avatars/ada.svg' },
  { name: 'Grace Hopper', initials: 'GH', src: '/avatars/grace.svg' },
  { name: 'Katherine Johnson', initials: 'KJ', src: '/avatars/katherine.svg' },
  { name: 'Mary Jackson', initials: 'MJ', src: '/avatars/mary.svg' },
  { name: 'Dorothy Vaughan', initials: 'DV', src: '/avatars/dorothy.svg' },
];

function TeamAvatar({
  person,
  status,
}: {
  person: { name: string; initials: string; src: string };
  status?: (typeof STATUS_VALUES)[number];
}) {
  return (
    <Avatar>
      <AvatarImage src={person.src} alt={person.name} />
      <AvatarFallback>{person.initials}</AvatarFallback>
      {status ? <AvatarStatus status={status} /> : null}
    </Avatar>
  );
}

export default function AvatarGroupWithMax() {
  return (
    <AvatarGroup max={3} role="group" aria-label="Project team">
      {PORTRAITS.map((person, index) => (
        <TeamAvatar
          key={person.name}
          person={person}
          status={STATUS_VALUES[index % STATUS_VALUES.length]}
        />
      ))}
    </AvatarGroup>
  );
}

All sizes

Compare every size, both shapes and the fallback states.

All Sizes (Circle)

2X2xs
XSxs
SMsm
MDmd
LGlg
XLxl
2XL2xl
3XL3xl
4XL4xl

Shapes

ALcircle
ALrounded

Fallback States

Initials
Icon

Status, Ring, and Group

ALOnline
ALOnlineGHAwayKJBusy
'use client';

import { UserIcon } from '@phosphor-icons/react/ssr';

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  AvatarStatus,
} from '@/components/nxds/components/avatar/avatar';

const AVATAR_SIZES = [
  '2xs',
  'xs',
  'sm',
  'md',
  'lg',
  'xl',
  '2xl',
  '3xl',
  '4xl',
] as const;

const STATUS_VALUES = ['online', 'away', 'busy', 'offline'] as const;

const PORTRAITS = [
  { name: 'Ada Lovelace', initials: 'AL', src: '/avatars/ada.svg' },
  { name: 'Grace Hopper', initials: 'GH', src: '/avatars/grace.svg' },
  { name: 'Katherine Johnson', initials: 'KJ', src: '/avatars/katherine.svg' },
  { name: 'Mary Jackson', initials: 'MJ', src: '/avatars/mary.svg' },
  { name: 'Dorothy Vaughan', initials: 'DV', src: '/avatars/dorothy.svg' },
];

function avatarLabel(size: (typeof AVATAR_SIZES)[number]) {
  if (size === '2xs') return '2X';
  if (size === 'xs') return 'XS';
  return size.toUpperCase();
}

function TeamAvatar({
  person,
  status,
}: {
  person: { name: string; initials: string; src: string };
  status?: (typeof STATUS_VALUES)[number];
}) {
  return (
    <Avatar>
      <AvatarImage src={person.src} alt={person.name} />
      <AvatarFallback>{person.initials}</AvatarFallback>
      {status ? <AvatarStatus status={status} /> : null}
    </Avatar>
  );
}

export default function AvatarAllSizes() {
  return (
    <div className="nx:flex nx:flex-col nx:gap-8">
      <div>
        <h3 className="nx:mb-4 nx:typography-label-default nx:text-foreground">
          All Sizes (Circle)
        </h3>
        <div className="nx:flex nx:items-end nx:gap-4">
          {AVATAR_SIZES.map((size) => (
            <div
              key={size}
              className="nx:flex nx:flex-col nx:items-center nx:gap-2"
            >
              <Avatar size={size}>
                <AvatarImage
                  src="/avatars/ada.svg"
                  alt={`Ada Lovelace ${size}`}
                />
                <AvatarFallback>{avatarLabel(size)}</AvatarFallback>
              </Avatar>
              <span className="nx:typography-label-small nx:text-muted-foreground">
                {size}
              </span>
            </div>
          ))}
        </div>
      </div>

      <div>
        <h3 className="nx:mb-4 nx:typography-label-default nx:text-foreground">
          Shapes
        </h3>
        <div className="nx:flex nx:items-center nx:gap-4">
          <div className="nx:flex nx:flex-col nx:items-center nx:gap-2">
            <Avatar size="xl" shape="circle">
              <AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace circle" />
              <AvatarFallback>AL</AvatarFallback>
            </Avatar>
            <span className="nx:typography-label-small nx:text-muted-foreground">
              circle
            </span>
          </div>
          <div className="nx:flex nx:flex-col nx:items-center nx:gap-2">
            <Avatar size="xl" shape="rounded">
              <AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace rounded" />
              <AvatarFallback>AL</AvatarFallback>
            </Avatar>
            <span className="nx:typography-label-small nx:text-muted-foreground">
              rounded
            </span>
          </div>
        </div>
      </div>

      <div>
        <h3 className="nx:mb-4 nx:typography-label-default nx:text-foreground">
          Fallback States
        </h3>
        <div className="nx:flex nx:items-center nx:gap-4">
          <div className="nx:flex nx:flex-col nx:items-center nx:gap-2">
            <Avatar size="xl" role="img" aria-label="Ada Lovelace">
              <AvatarFallback aria-hidden="true">AL</AvatarFallback>
            </Avatar>
            <span className="nx:typography-label-small nx:text-muted-foreground">
              Initials
            </span>
          </div>
          <div className="nx:flex nx:flex-col nx:items-center nx:gap-2">
            <Avatar size="xl" role="img" aria-label="Unknown user">
              <AvatarFallback aria-hidden="true">
                <UserIcon aria-hidden="true" className="nx:size-6" />
              </AvatarFallback>
            </Avatar>
            <span className="nx:typography-label-small nx:text-muted-foreground">
              Icon
            </span>
          </div>
        </div>
      </div>

      <div>
        <h3 className="nx:mb-4 nx:typography-label-default nx:text-foreground">
          Status, Ring, and Group
        </h3>
        <div className="nx:flex nx:flex-wrap nx:items-center nx:gap-6">
          <Avatar size="xl" ring>
            <AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace selected" />
            <AvatarFallback>AL</AvatarFallback>
            <AvatarStatus status="online" />
          </Avatar>
          <AvatarGroup max={3}>
            {PORTRAITS.map((person, index) => (
              <TeamAvatar
                key={person.name}
                person={person}
                status={STATUS_VALUES[index % STATUS_VALUES.length]}
              />
            ))}
          </AvatarGroup>
        </div>
      </div>
    </div>
  );
}

This example also needs:

npm install @phosphor-icons/react@^2.1.10

Props

Avatar

Avatar props
PropTypeDefaultDescription
ringbooleanfalse

Draws a non-interactive emphasis ring, such as for a selected person. Put focus rings on the interactive wrapper.

shape"rounded" | "circle" | null"circle"No description
size"sm" | "lg" | "2xs" | "xs" | "md" | "xl" | "2xl" | "3xl" | "4xl" | null"md"No description

AvatarFallback

AvatarFallback adds no props of its own; it accepts the props of the element or primitive it wraps.

AvatarGroup

AvatarGroup props
PropTypeDefaultDescription
maxnumberNo default documented

Number of avatars shown before the rest collapse into a +N tile.

shape"rounded" | "circle" | null"circle"No description
size"sm" | "lg" | "2xs" | "xs" | "md" | "xl" | "2xl" | "3xl" | "4xl" | null"md"No description

AvatarImage

AvatarImage adds no props of its own; it accepts the props of the element or primitive it wraps.

AvatarStatus

AvatarStatus props
PropTypeDefaultDescription
labelstringNo default documented

Screen reader text for the dot, the capitalized status by default. Pass '' when visible text already shows presence.

status"online" | "away" | "busy" | "offline" | null"online"No description

Avatar shows a person's profile image, with initials as the fallback. Use AvatarGroup to stack several people and AvatarStatus to show presence.

<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from '@/components/avatar';
</script>

<template>
  <Avatar>
    <AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace" />
    <AvatarFallback>AL</AvatarFallback>
  </Avatar>
</template>

Installation

  1. Install the following dependencies:

    npm install @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0
    
  2. Copy and paste the following code into your project.

    components/avatar/Avatar.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import { AvatarRoot, type AvatarRootProps } from 'reka-ui';
    import { computed, type HTMLAttributes, inject } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { AvatarGroupKey, type AvatarVariants, avatarVariants } from '.';
    
    export interface AvatarProps extends AvatarRootProps {
      size?: AvatarVariants['size'];
      shape?: AvatarVariants['shape'];
      /**
       * Draws a non-interactive emphasis ring, such as for a selected person. Put focus rings on the interactive wrapper.
       */
      ring?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<AvatarProps>(), { ring: false });
    
    const delegated = reactiveOmit(props, 'class', 'size', 'shape', 'ring');
    
    const group = inject(AvatarGroupKey, undefined);
    const size = computed(() => props.size ?? group?.size);
    const shape = computed(() => props.shape ?? group?.shape);
    </script>
    
    <template>
      <AvatarRoot
        v-bind="delegated"
        data-slot="avatar"
        :data-size="size ?? 'md'"
        :data-shape="shape ?? 'circle'"
        :data-ring="ring || undefined"
        :class="
          cn(
            avatarVariants({ size, shape }),
            ring &&
              'nx:ring-2 nx:ring-primary-border nx:ring-offset-2 nx:ring-offset-surface',
            props.class
          )
        "
      >
        <slot />
      </AvatarRoot>
    </template>
    
    components/avatar/AvatarFallback.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      AvatarFallback,
      type AvatarFallbackProps as RekaAvatarFallbackProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface AvatarFallbackProps extends RekaAvatarFallbackProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<AvatarFallbackProps>();
    
    const delegated = reactiveOmit(props, 'class');
    </script>
    
    <template>
      <AvatarFallback
        v-bind="delegated"
        data-slot="avatar-fallback"
        :class="
          cn(
            'nx:flex nx:size-full nx:items-center nx:justify-center nx:rounded-[inherit] nx:bg-muted nx:text-foreground nx:font-medium nx:leading-none',
            props.class
          )
        "
      >
        <slot />
      </AvatarFallback>
    </template>
    
    components/avatar/AvatarGroup.vue
    <script setup lang="ts">
    import { type HTMLAttributes, provide, type VNode } from 'vue';
    
    import { flattenedNodes } from '../../lib/slots';
    import { cn } from '../../lib/utils';
    
    import Avatar from './Avatar.vue';
    import AvatarFallback from './AvatarFallback.vue';
    
    import { AvatarGroupKey, type AvatarVariants } from '.';
    
    export interface AvatarGroupProps {
      size?: AvatarVariants['size'];
      shape?: AvatarVariants['shape'];
      /**
       * Number of avatars shown before the rest collapse into a +N tile.
       */
      max?: number;
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<AvatarGroupProps>();
    
    const slots = defineSlots<{ default?: () => VNode[] }>();
    
    provide(AvatarGroupKey, props);
    
    function splitMembers() {
      const members = flattenedNodes(slots.default?.());
      const hasMax = typeof props.max === 'number' && props.max > 0;
      const visible = hasMax ? members.slice(0, props.max) : members;
      return { visible, overflow: members.length - visible.length };
    }
    </script>
    
    <template>
      <div
        data-slot="avatar-group"
        :data-size="size"
        :data-shape="shape"
        :class="
          cn(
            'nx:flex nx:items-center',
            // Overlap scales with each avatar's font-size (set per size), so the
            // tuck stays ~proportional from 2xs to 4xl instead of a fixed px.
            'nx:[&>[data-slot=avatar]+[data-slot=avatar]]:ms-[-0.7em]',
            'nx:*:data-[slot=avatar]:relative',
            'nx:*:data-[slot=avatar]:ring-[0.1em] nx:*:data-[slot=avatar]:ring-surface',
            props.class
          )
        "
      >
        <!-- A one-item v-for runs the default slot once per render. -->
        <template
          v-for="({ visible, overflow }, split) in [splitMembers()]"
          :key="split"
        >
          <component
            :is="member"
            v-for="(member, index) in visible"
            :key="member.key ?? index"
          />
          <Avatar v-if="overflow > 0" role="img" :aria-label="`${overflow} more`">
            <AvatarFallback aria-hidden="true">+{{ overflow }}</AvatarFallback>
          </Avatar>
        </template>
      </div>
    </template>
    
    components/avatar/AvatarImage.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      AvatarImage,
      type AvatarImageEmits,
      type AvatarImageProps as RekaAvatarImageProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface AvatarImageProps extends RekaAvatarImageProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<AvatarImageProps>();
    const emits = defineEmits<AvatarImageEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <AvatarImage
        v-bind="forwarded"
        data-slot="avatar-image"
        decoding="async"
        :class="
          cn(
            'nx:aspect-square nx:size-full nx:rounded-[inherit] nx:object-cover',
            'nx:animate-in nx:fade-in-0 nx:duration-fast nx:ease-enter',
            props.class
          )
        "
      />
    </template>
    
    components/avatar/AvatarStatus.vue
    <script setup lang="ts">
    import { computed, type HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import {
      type AvatarStatusVariants,
      avatarStatusVariants,
      STATUS_LABELS,
    } from '.';
    
    export interface AvatarStatusProps {
      status?: AvatarStatusVariants['status'];
      /**
       * Screen reader text for the dot, the capitalized `status` by default. Pass `''` when visible text already shows presence.
       */
      label?: string;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<AvatarStatusProps>(), {
      status: 'online',
    });
    
    const resolvedLabel = computed(
      () => props.label ?? STATUS_LABELS[props.status ?? 'online']
    );
    </script>
    
    <template>
      <span
        data-slot="avatar-status"
        :data-status="status"
        :class="cn(avatarStatusVariants({ status }), props.class)"
      >
        <span class="nx:sr-only">{{ resolvedLabel }}</span>
      </span>
    </template>
    
    components/avatar/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    import type { InjectionKey } from 'vue';
    
    import type { AvatarGroupProps } from './AvatarGroup.vue';
    
    export { default as Avatar, type AvatarProps } from './Avatar.vue';
    export {
      default as AvatarFallback,
      type AvatarFallbackProps,
    } from './AvatarFallback.vue';
    export {
      default as AvatarGroup,
      type AvatarGroupProps,
    } from './AvatarGroup.vue';
    export {
      default as AvatarImage,
      type AvatarImageProps,
    } from './AvatarImage.vue';
    export {
      default as AvatarStatus,
      type AvatarStatusProps,
    } from './AvatarStatus.vue';
    
    // Avatar falls back to this for `size`/`shape` when its own prop is unset.
    export const AvatarGroupKey: InjectionKey<
      Pick<AvatarGroupProps, 'size' | 'shape'>
    > = Symbol('avatar-group');
    
    export const avatarVariants = cva(
      'nx:relative nx:flex nx:shrink-0 nx:after:pointer-events-none nx:after:absolute nx:after:inset-0 nx:after:rounded-[inherit] nx:after:outline nx:after:outline-1 nx:after:-outline-offset-1 nx:after:outline-border-hairline',
      {
        variants: {
          size: {
            '2xs':
              'nx:size-5 nx:text-[length:calc(var(--nx-typography-size-base)*0.625)]',
            xs: 'nx:size-6 nx:text-[length:calc(var(--nx-typography-size-base)*0.6875)]',
            sm: 'nx:size-8 nx:text-[length:calc(var(--nx-typography-size-base)*0.75)]',
            md: 'nx:size-10 nx:text-[length:calc(var(--nx-typography-size-base)*1)]',
            lg: 'nx:size-12 nx:text-[length:calc(var(--nx-typography-size-base)*1.125)]',
            xl: 'nx:size-14 nx:text-[length:calc(var(--nx-typography-size-base)*1.25)]',
            '2xl':
              'nx:size-16 nx:text-[length:calc(var(--nx-typography-size-base)*1.5)]',
            '3xl':
              'nx:size-20 nx:text-[length:calc(var(--nx-typography-size-base)*1.875)]',
            '4xl':
              'nx:size-24 nx:text-[length:calc(var(--nx-typography-size-base)*2.25)]',
          },
          shape: {
            circle: 'nx:rounded-full',
            rounded: 'nx:rounded-lg',
          },
        },
        defaultVariants: {
          size: 'md',
          shape: 'circle',
        },
      }
    );
    
    export const avatarStatusVariants = cva(
      'nx:absolute nx:right-[0.04em] nx:bottom-[0.04em] nx:size-[0.6em] nx:rounded-full nx:ring-[0.1em] nx:ring-surface',
      {
        variants: {
          status: {
            online: 'nx:bg-success-background',
            away: 'nx:bg-warning-background',
            busy: 'nx:bg-error-background',
            offline: 'nx:bg-muted',
          },
        },
        defaultVariants: {
          status: 'online',
        },
      }
    );
    
    export const STATUS_LABELS = {
      online: 'Online',
      away: 'Away',
      busy: 'Busy',
      offline: 'Offline',
    } as const satisfies Record<
      NonNullable<VariantProps<typeof avatarStatusVariants>['status']>,
      string
    >;
    
    export type AvatarVariants = VariantProps<typeof avatarVariants>;
    export type AvatarStatusVariants = VariantProps<typeof avatarStatusVariants>;
    
    lib/slots.ts
    import { Comment, Fragment, Text, type VNode } from 'vue';
    
    /** The nodes a slot renders, without comments and whitespace-only text. */
    export function renderedNodes(nodes: VNode[] | undefined) {
      return (nodes ?? []).filter(
        (node) =>
          node.type !== Comment &&
          !(node.type === Text && !String(node.children).trim())
      );
    }
    
    /** {@link renderedNodes} with each `v-for` or `<template>` fragment unwrapped into its children. */
    export function flattenedNodes(nodes: VNode[] | undefined): VNode[] {
      return renderedNodes(nodes).flatMap((node) =>
        node.type === Fragment && Array.isArray(node.children)
          ? flattenedNodes(node.children as VNode[])
          : [node]
      );
    }
    
  3. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Avatar, AvatarFallback, AvatarGroup, AvatarImage, AvatarStatus } from '@/components/avatar';

Examples

Status variants

Show online, away, busy and offline presence dots.

<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarStatus } from '@/components/avatar';

const people = [
  { initials: 'AL', status: 'online' },
  { initials: 'GH', status: 'away' },
  { initials: 'KJ', status: 'busy' },
  { initials: 'MJ', status: 'offline' },
] as const;
</script>

<template>
  <div class="nx:flex nx:items-center nx:gap-4">
    <Avatar v-for="person in people" :key="person.status" size="lg">
      <AvatarFallback>{{ person.initials }}</AvatarFallback>
      <AvatarStatus :status="person.status" />
    </Avatar>
  </div>
</template>

Group with max

Collapse extra avatars into a +N tile with max.

<script setup lang="ts">
import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  AvatarStatus,
} from '@/components/avatar';

const people = [
  {
    name: 'Ada Lovelace',
    initials: 'AL',
    src: '/avatars/ada.svg',
    status: 'online',
  },
  {
    name: 'Grace Hopper',
    initials: 'GH',
    src: '/avatars/grace.svg',
    status: 'away',
  },
  {
    name: 'Katherine Johnson',
    initials: 'KJ',
    src: '/avatars/katherine.svg',
    status: 'busy',
  },
  {
    name: 'Mary Jackson',
    initials: 'MJ',
    src: '/avatars/mary.svg',
    status: 'offline',
  },
  {
    name: 'Dorothy Vaughan',
    initials: 'DV',
    src: '/avatars/dorothy.svg',
    status: 'online',
  },
] as const;
</script>

<template>
  <AvatarGroup :max="3" role="group" aria-label="Project team">
    <Avatar v-for="person in people" :key="person.name">
      <AvatarImage :src="person.src" :alt="person.name" />
      <AvatarFallback>{{ person.initials }}</AvatarFallback>
      <AvatarStatus :status="person.status" />
    </Avatar>
  </AvatarGroup>
</template>

All sizes

Compare every size, both shapes and the fallback states.

<script setup lang="ts">
import { PhUser } from '@phosphor-icons/vue';

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  AvatarStatus,
} from '@/components/avatar';

const sizes = [
  '2xs',
  'xs',
  'sm',
  'md',
  'lg',
  'xl',
  '2xl',
  '3xl',
  '4xl',
] as const;
const people = [
  {
    name: 'Ada Lovelace',
    initials: 'AL',
    src: '/avatars/ada.svg',
    status: 'online',
  },
  {
    name: 'Grace Hopper',
    initials: 'GH',
    src: '/avatars/grace.svg',
    status: 'away',
  },
  {
    name: 'Katherine Johnson',
    initials: 'KJ',
    src: '/avatars/katherine.svg',
    status: 'busy',
  },
  {
    name: 'Mary Jackson',
    initials: 'MJ',
    src: '/avatars/mary.svg',
    status: 'offline',
  },
  {
    name: 'Dorothy Vaughan',
    initials: 'DV',
    src: '/avatars/dorothy.svg',
    status: 'online',
  },
] as const;
</script>

<template>
  <div class="nx:flex nx:flex-col nx:gap-8">
    <div>
      <h3 class="nx:mb-4 nx:typography-label-default nx:text-foreground">
        All Sizes (Circle)
      </h3>
      <div class="nx:flex nx:items-end nx:gap-4">
        <div
          v-for="size in sizes"
          :key="size"
          class="nx:flex nx:flex-col nx:items-center nx:gap-2"
        >
          <Avatar :size="size">
            <AvatarImage src="/avatars/ada.svg" :alt="'Ada Lovelace ' + size" />
            <AvatarFallback>{{
              size === '2xs' ? '2X' : size.toUpperCase()
            }}</AvatarFallback>
          </Avatar>
          <span class="nx:typography-label-small nx:text-muted-foreground">{{
            size
          }}</span>
        </div>
      </div>
    </div>

    <div>
      <h3 class="nx:mb-4 nx:typography-label-default nx:text-foreground">
        Shapes
      </h3>
      <div class="nx:flex nx:items-center nx:gap-4">
        <div class="nx:flex nx:flex-col nx:items-center nx:gap-2">
          <Avatar size="xl" shape="circle">
            <AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace circle" />
            <AvatarFallback>AL</AvatarFallback>
          </Avatar>
          <span class="nx:typography-label-small nx:text-muted-foreground"
            >circle</span
          >
        </div>
        <div class="nx:flex nx:flex-col nx:items-center nx:gap-2">
          <Avatar size="xl" shape="rounded">
            <AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace rounded" />
            <AvatarFallback>AL</AvatarFallback>
          </Avatar>
          <span class="nx:typography-label-small nx:text-muted-foreground"
            >rounded</span
          >
        </div>
      </div>
    </div>

    <div>
      <h3 class="nx:mb-4 nx:typography-label-default nx:text-foreground">
        Fallback States
      </h3>
      <div class="nx:flex nx:items-center nx:gap-4">
        <div class="nx:flex nx:flex-col nx:items-center nx:gap-2">
          <Avatar size="xl" role="img" aria-label="Ada Lovelace">
            <AvatarFallback aria-hidden="true">AL</AvatarFallback>
          </Avatar>
          <span class="nx:typography-label-small nx:text-muted-foreground"
            >Initials</span
          >
        </div>
        <div class="nx:flex nx:flex-col nx:items-center nx:gap-2">
          <Avatar size="xl" role="img" aria-label="Unknown user">
            <AvatarFallback aria-hidden="true">
              <PhUser aria-hidden="true" class="nx:size-6" />
            </AvatarFallback>
          </Avatar>
          <span class="nx:typography-label-small nx:text-muted-foreground"
            >Icon</span
          >
        </div>
      </div>
    </div>

    <div>
      <h3 class="nx:mb-4 nx:typography-label-default nx:text-foreground">
        Status, Ring, and Group
      </h3>
      <div class="nx:flex nx:flex-wrap nx:items-center nx:gap-6">
        <Avatar size="xl" ring>
          <AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace selected" />
          <AvatarFallback>AL</AvatarFallback>
          <AvatarStatus status="online" />
        </Avatar>
        <AvatarGroup :max="3">
          <Avatar v-for="person in people" :key="person.name">
            <AvatarImage :src="person.src" :alt="person.name" />
            <AvatarFallback>{{ person.initials }}</AvatarFallback>
            <AvatarStatus :status="person.status" />
          </Avatar>
        </AvatarGroup>
      </div>
    </div>
  </div>
</template>

This example also needs:

npm install @phosphor-icons/vue@^2.2.1

Props

Avatar

Avatar props
PropTypeDefaultDescription
ringbooleanfalse

Draws a non-interactive emphasis ring, such as for a selected person. Put focus rings on the interactive wrapper.

shape"circle" | "rounded" | null"circle"No description
size"sm" | "lg" | "2xs" | "xs" | "md" | "xl" | "2xl" | "3xl" | "4xl" | null"md"No description

AvatarFallback

AvatarFallback adds no props of its own; it accepts the props of the element or primitive it wraps.

AvatarGroup

AvatarGroup props
PropTypeDefaultDescription
maxnumberNo default documented

Number of avatars shown before the rest collapse into a +N tile.

shape"circle" | "rounded" | null"circle"No description
size"sm" | "lg" | "2xs" | "xs" | "md" | "xl" | "2xl" | "3xl" | "4xl" | null"md"No description

AvatarImage

AvatarImage adds no props of its own; it accepts the props of the element or primitive it wraps.

AvatarStatus

AvatarStatus props
PropTypeDefaultDescription
labelstringNo default documented

Screen reader text for the dot, the capitalized status by default. Pass '' when visible text already shows presence.

status"online" | "away" | "busy" | "offline" | null"online"No description