UIPackage

Avatar

Vue data-display
Edit on GitHub

Round or rounded-square user image with a fallback that shows initials or an icon when the image is missing or fails to load. Sizes from xs to 2xl, optional status dot, and a group composition for stacked avatar lists.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/avatar.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/avatar

Examples

Props

Name Type / Values Default Required
class HTMLAttributes['class'] optional
size
'xs''sm''default''lg''xl''2xl'
optional
rounded
'none''sm''default''md''lg''xl''2xl''3xl''full'
optional
color
'default''primary''secondary''destructive''success''warning''info''error''muted'
optional
variant
'default''outlined''soft'
optional
tile boolean false optional
disabled boolean false optional
loading boolean false optional

Dependencies

Used by

Files (6)

  • app/components/ui/avatar/Avatar.vue 1.6 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { computed } from 'vue'
    import { cn } from '@/lib/utils'
    import { avatarVariants } from './avatar.variants'
    
    // Inlined unions: SFC compiler can't extract runtime props from
    // `AvatarVariants['size']` etc. indexed-access types.
    const props = withDefaults(
      defineProps<{
        class?: HTMLAttributes['class']
        size?: 'xs' | 'sm' | 'default' | 'lg' | 'xl' | '2xl'
        rounded?: 'none' | 'sm' | 'default' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full'
        color?: 'default' | 'primary' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info' | 'error' | 'muted'
        variant?: 'default' | 'outlined' | 'soft'
        tile?: boolean
        disabled?: boolean
        loading?: boolean
      }>(),
      {
        tile: false,
        disabled: false,
        loading: false,
      },
    )
    
    const emit = defineEmits<{
      click: [event: MouseEvent]
      error: [event: Event]
      load: [event: Event]
    }>()
    
    function handleClick(event: MouseEvent) {
      if (!props.disabled) {
        emit('click', event)
      }
    }
    
    function handleError(event: Event) {
      emit('error', event)
    }
    
    function handleLoad(event: Event) {
      emit('load', event)
    }
    
    const rootClasses = computed(() =>
      cn(
        avatarVariants({ size: props.size, rounded: props.rounded, color: props.color, variant: props.variant }),
        props.tile ? 'rounded-none' : '',
        props.disabled ? 'opacity-50 cursor-not-allowed' : '',
        props.loading ? 'animate-pulse' : '',
        props.class,
      ),
    )
    </script>
    
    <template>
      <span :class="rootClasses" data-uipkge data-slot="avatar" @click="handleClick">
        <slot />
      </span>
    </template>
  • app/components/ui/avatar/AvatarFallback.vue 1 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { computed } from 'vue'
    import { cn } from '@/lib/utils'
    import { avatarFallbackVariants } from './avatar.variants'
    
    // Inlined unions: SFC compiler can't extract runtime props from
    // indexed-access types.
    const props = withDefaults(
      defineProps<{
        class?: HTMLAttributes['class']
        size?: 'xs' | 'sm' | 'default' | 'lg' | 'xl' | '2xl'
        color?: 'default' | 'primary' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info' | 'error' | 'muted'
        text?: string
      }>(),
      {
        size: 'default',
        color: 'default',
      },
    )
    
    const emit = defineEmits<{
      click: [event: MouseEvent]
    }>()
    
    function handleClick(event: MouseEvent) {
      emit('click', event)
    }
    
    const rootClasses = computed(() => cn(avatarFallbackVariants({ size: props.size, color: props.color }), props.class))
    </script>
    
    <template>
      <span :class="rootClasses" data-uipkge data-slot="avatar-fallback" @click="handleClick">
        <template v-if="text">{{ text }}</template>
        <slot v-else />
      </span>
    </template>
  • app/components/ui/avatar/AvatarGroup.vue 1.7 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    export interface AvatarGroupProps {
      class?: HTMLAttributes['class']
      max?: number
      overlap?: boolean
      size?: 'xs' | 'sm' | 'default' | 'lg' | 'xl' | '2xl'
      total?: number
    }
    
    const props = withDefaults(defineProps<AvatarGroupProps>(), {
      overlap: true,
      size: 'default',
    })
    
    const emit = defineEmits<{
      click: [event: MouseEvent]
    }>()
    
    function handleClick(event: MouseEvent) {
      emit('click', event)
    }
    </script>
    
    <template>
      <div
        :class="cn('flex items-center', overlap ? '-space-x-2' : 'gap-1', props.class)"
        data-uipkge
        data-slot="avatar-group"
        @click="handleClick"
      >
        <slot :max="max" :size="size" />
        <template v-if="max && ($slots.default?.()?.length || 0) > max">
          <div
            :class="
              cn(
                'bg-muted ring-background relative flex shrink-0 overflow-hidden rounded-full ring-2',
                size === 'xs'
                  ? 'size-4 text-[8px]'
                  : size === 'sm'
                    ? 'size-6 text-xs'
                    : size === 'default'
                      ? 'size-8 text-sm'
                      : size === 'lg'
                        ? 'size-12 text-base'
                        : size === 'xl'
                          ? 'size-16 text-lg'
                          : 'size-20 text-xl',
              )
            "
          >
            <slot name="overflow" :count="($slots.default?.()?.length || 0) - max + 1" />
            <span v-if="!$slots['overflow']" class="flex size-full items-center justify-center font-medium">
              +{{ ($slots.default?.()?.length || 0) - max + 1 }}
            </span>
          </div>
        </template>
      </div>
    </template>
  • app/components/ui/avatar/AvatarImage.vue 1.7 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { ref, watch } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = withDefaults(
      defineProps<{
        class?: HTMLAttributes['class']
        src?: string
        alt?: string
        fallback?: string
        loading?: 'eager' | 'lazy'
        referrerpolicy?:
          | 'no-referrer'
          | 'no-referrer-when-downgrade'
          | 'origin'
          | 'origin-when-cross-origin'
          | 'same-origin'
          | 'strict-origin'
          | 'strict-origin-when-cross-origin'
          | 'unsafe-url'
          | ''
        crossorigin?: 'anonymous' | 'use-credentials'
        onerror?: (event: Event) => void
        onload?: (event: Event) => void
      }>(),
      {
        loading: 'lazy',
      },
    )
    
    const emit = defineEmits<{
      error: [event: Event]
      load: [event: Event]
    }>()
    
    // Track <img> failures so a broken URL falls back to the slot
    // (consumer renders AvatarFallback there). Without this, an HTTP 404
    // leaves the broken-image glyph forever, which contradicts the docs
    // promise that AvatarFallback shows when the image fails.
    const errored = ref(false)
    watch(
      () => props.src,
      () => {
        errored.value = false
      },
    )
    
    function handleError(event: Event) {
      errored.value = true
      emit('error', event)
      if (props.onerror) {
        props.onerror(event)
      }
    }
    
    function handleLoad(event: Event) {
      emit('load', event)
      if (props.onload) {
        props.onload(event)
      }
    }
    </script>
    
    <template>
      <img
        v-if="src && !errored"
        :src="src"
        :alt="alt"
        :loading="loading"
        :referrerpolicy="referrerpolicy"
        :crossorigin="crossorigin"
        :class="cn('aspect-square size-full object-cover', props.class)"
        data-uipkge
        data-slot="avatar-image"
        @error="handleError"
        @load="handleLoad"
      />
      <slot v-else />
    </template>
  • app/components/ui/avatar/avatar.variants.ts 3.4 kB
    import type { VariantProps } from 'class-variance-authority'
    import { cva } from 'class-variance-authority'
    
    /**
     * Variant definitions live in their own file (rather than the package
     * `index.ts` or inline in the SFC) so `Avatar.vue` / `AvatarFallback.vue`
     * can `import { avatarVariants } from './avatar.variants'` without
     * creating a circular dependency back through the index. The circular
     * form caused intermittent `$setup.avatarVariants is not a function`
     * errors during dev SSR.
     */
    export const avatarVariants = cva('relative flex shrink-0 overflow-hidden', {
      variants: {
        size: {
          xs: 'size-4',
          sm: 'size-6',
          default: 'size-8',
          lg: 'size-12',
          xl: 'size-16',
          '2xl': 'size-20',
        },
        rounded: {
          none: 'rounded-none',
          sm: 'rounded-sm',
          default: 'rounded-full',
          md: 'rounded-md',
          lg: 'rounded-lg',
          xl: 'rounded-xl',
          '2xl': 'rounded-2xl',
          '3xl': 'rounded-3xl',
          full: 'rounded-full',
        },
        color: {
          default: '',
          primary: 'bg-primary text-primary-foreground',
          secondary: 'bg-secondary text-secondary-foreground',
          destructive: 'bg-destructive text-destructive-foreground',
          success: 'bg-[var(--success)] text-white dark:text-black',
          warning: 'bg-[var(--warning)] text-black',
          info: 'bg-[var(--info)] text-white dark:text-black',
          error: 'bg-destructive text-white dark:text-black',
          muted: 'bg-muted text-muted-foreground',
        },
        variant: {
          default: '',
          outlined: 'border-2 border-current',
          soft: 'bg-opacity-20',
        },
      },
      compoundVariants: [
        { color: 'primary', variant: 'soft', class: 'bg-primary/20 text-primary' },
        { color: 'secondary', variant: 'soft', class: 'bg-secondary/20 text-secondary-foreground' },
        { color: 'destructive', variant: 'soft', class: 'bg-destructive/20 text-destructive' },
        { color: 'success', variant: 'soft', class: 'bg-[var(--success)]/20 text-[var(--success)]' },
        { color: 'warning', variant: 'soft', class: 'bg-[var(--warning)]/20 text-[var(--warning)]' },
        { color: 'info', variant: 'soft', class: 'bg-[var(--info)]/20 text-[var(--info)]' },
        { color: 'error', variant: 'soft', class: 'bg-destructive/20 text-destructive' },
      ],
      defaultVariants: {
        size: 'default',
        rounded: 'default',
        color: 'default',
      },
    })
    
    export type AvatarVariants = VariantProps<typeof avatarVariants>
    
    export const avatarFallbackVariants = cva(
      'flex size-full items-center justify-center rounded-full bg-muted font-medium',
      {
        variants: {
          size: {
            xs: 'text-[8px]',
            sm: 'text-xs',
            default: 'text-sm',
            lg: 'text-base',
            xl: 'text-lg',
            '2xl': 'text-xl',
          },
          color: {
            default: '',
            primary: 'bg-primary text-primary-foreground',
            secondary: 'bg-secondary text-secondary-foreground',
            destructive: 'bg-destructive text-destructive-foreground',
            success: 'bg-[var(--success)] text-white dark:text-black',
            warning: 'bg-[var(--warning)] text-black',
            info: 'bg-[var(--info)] text-white dark:text-black',
            error: 'bg-destructive text-white dark:text-black',
            muted: 'bg-muted text-muted-foreground',
          },
        },
        defaultVariants: {
          size: 'default',
          color: 'default',
        },
      },
    )
    
    export type AvatarFallbackVariants = VariantProps<typeof avatarFallbackVariants>
  • app/components/ui/avatar/index.ts 0.5 kB
    export { default as Avatar } from './Avatar.vue'
    export { default as AvatarFallback } from './AvatarFallback.vue'
    export { default as AvatarImage } from './AvatarImage.vue'
    export { default as AvatarGroup } from './AvatarGroup.vue'
    
    // Re-export variant API from the sibling file (kept separate to avoid the
    // Avatar.vue <-> index.ts circular import that broke dev SSR).
    export {
      avatarVariants,
      avatarFallbackVariants,
      type AvatarVariants,
      type AvatarFallbackVariants,
    } from './avatar.variants'

Raw manifest: https://uipkge.dev/r/vue/avatar.json