Avatar
avatar ui 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
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/avatar.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/avatar.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/avatar.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/avatar.json Named registry:
npx shadcn-vue@latest add @uipkge/avatar Installs to: app/components/ui/avatar/ Examples
Loading interactive previews…
Loading interactive previews…
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 |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
AvatarContext interface AvatarContext {
imageStatus: Ref<AvatarImageStatus>
setImageStatus: (status: AvatarImageStatus) => void
} npm dependencies
Used by
Files installed (7)
-
app/components/ui/avatar/Avatar.vue 1.7 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, provide, ref } from 'vue' import { cn } from '@/lib/utils' import { avatarVariants } from './avatar.variants' import { AVATAR_INJECTION_KEY, type AvatarImageStatus } from './context' // 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] }>() function handleClick(event: MouseEvent) { if (!props.disabled) { emit('click', event) } } const imageStatus = ref<AvatarImageStatus>('idle') provide(AVATAR_INJECTION_KEY, { imageStatus, setImageStatus: (status) => { imageStatus.value = status }, }) const rootClasses = computed(() => cn( avatarVariants({ size: props.size, rounded: props.rounded, color: props.color, variant: props.variant }), props.tile ? 'rounded-none' : '', props.disabled ? 'cursor-not-allowed opacity-50' : '', 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.3 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, inject } from 'vue' import { cn } from '@/lib/utils' import { avatarFallbackVariants } from './avatar.variants' import { AVATAR_INJECTION_KEY } from './context' // 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) } // Show fallback until a sibling AvatarImage reports loaded (Radix composition). const ctx = inject(AVATAR_INJECTION_KEY, null) const visible = computed(() => !ctx || ctx.imageStatus.value !== 'loaded') const rootClasses = computed(() => cn(avatarFallbackVariants({ size: props.size, color: props.color }), props.class)) </script> <template> <span v-if="visible" :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 2.9 kB
<script setup lang="ts"> import type { HTMLAttributes, VNode } from 'vue' import { Comment, Fragment, Text, computed, defineComponent, useSlots } from 'vue' import { cn } from '@/lib/utils' export interface AvatarGroupProps { class?: HTMLAttributes['class'] max?: number overlap?: boolean size?: 'xs' | 'sm' | 'default' | 'lg' | 'xl' | '2xl' /** Override the total used for +N when only a subset of avatars is rendered. */ total?: number } const props = withDefaults(defineProps<AvatarGroupProps>(), { overlap: true, size: 'default', }) const emit = defineEmits<{ click: [event: MouseEvent] }>() function handleClick(event: MouseEvent) { emit('click', event) } const slots = useSlots() function flattenVNodes(nodes: VNode[] | undefined): VNode[] { const out: VNode[] = [] for (const node of nodes ?? []) { if (!node) continue if (node.type === Comment) continue if (node.type === Text && !String(node.children ?? '').trim()) continue if (node.type === Fragment) { out.push(...flattenVNodes(node.children as VNode[])) } else { out.push(node) } } return out } const allChildren = computed(() => flattenVNodes(slots.default?.() as VNode[] | undefined)) const totalCount = computed(() => props.total ?? allChildren.value.length) // When overflowing, reserve one slot for the +N chip so max includes the badge. const isOverflowing = computed(() => props.max != null && totalCount.value > props.max) const visibleLimit = computed(() => (isOverflowing.value ? Math.max((props.max ?? 0) - 1, 0) : allChildren.value.length)) const overflowCount = computed(() => (isOverflowing.value ? totalCount.value - visibleLimit.value : 0)) const visibleChildren = computed(() => allChildren.value.slice(0, visibleLimit.value)) const overflowSizeClass = computed(() => { switch (props.size) { case 'xs': return 'size-4 text-[8px]' case 'sm': return 'size-6 text-xs' case 'lg': return 'size-12 text-base' case 'xl': return 'size-16 text-lg' case '2xl': return 'size-20 text-xl' default: return 'size-8 text-sm' } }) // Render pre-flattened VNodes from the default slot. const VisibleAvatars = defineComponent({ name: 'AvatarGroupVisible', setup() { return () => visibleChildren.value }, }) </script> <template> <div :class="cn('flex items-center', overlap ? '-space-x-2' : 'gap-1', props.class)" data-uipkge data-slot="avatar-group" @click="handleClick" > <VisibleAvatars /> <div v-if="isOverflowing" :class=" cn( 'bg-muted ring-background relative flex shrink-0 overflow-hidden rounded-full ring-2', overflowSizeClass, ) " > <slot name="overflow" :count="overflowCount"> <span class="flex size-full items-center justify-center font-medium">+{{ overflowCount }}</span> </slot> </div> </div> </template> -
app/components/ui/avatar/AvatarImage.vue 1.7 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, inject, onBeforeUnmount, watch } from 'vue' import { cn } from '@/lib/utils' import { AVATAR_INJECTION_KEY } from './context' const props = withDefaults( defineProps<{ class?: HTMLAttributes['class'] src?: string alt?: 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' }>(), { loading: 'lazy', }, ) const emit = defineEmits<{ error: [event: Event] load: [event: Event] }>() // Sibling AvatarFallback shows when image is not loaded — match Radix composition. const ctx = inject(AVATAR_INJECTION_KEY, null) const status = computed(() => ctx?.imageStatus.value ?? 'idle') watch( () => props.src, (src) => { ctx?.setImageStatus(src ? 'loading' : 'idle') }, { immediate: true }, ) onBeforeUnmount(() => { ctx?.setImageStatus('idle') }) function handleError(event: Event) { ctx?.setImageStatus('error') emit('error', event) } function handleLoad(event: Event) { ctx?.setImageStatus('loaded') emit('load', event) } </script> <template> <img v-if="src && status !== 'error'" :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" /> </template> -
app/components/ui/avatar/avatar.variants.ts 3.3 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-success text-white dark:text-black', warning: 'bg-warning text-black', info: 'bg-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-success/20 text-success' }, { color: 'warning', variant: 'soft', class: 'bg-warning/20 text-warning' }, { color: 'info', variant: 'soft', class: 'bg-info/20 text-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-success text-white dark:text-black', warning: 'bg-warning text-black', info: 'bg-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/context.ts 0.3 kB
import type { InjectionKey, Ref } from 'vue' export type AvatarImageStatus = 'idle' | 'loading' | 'loaded' | 'error' export interface AvatarContext { imageStatus: Ref<AvatarImageStatus> setImageStatus: (status: AvatarImageStatus) => void } export const AVATAR_INJECTION_KEY: InjectionKey<AvatarContext> = Symbol('uipkge-avatar') -
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