Avatar Group

Displays a group of Avatar components.

Examples

Basic

PropDefaultTypeDescription
maxnumberThe maximum number of avatars to display before the rest are hidden.
Preview
Code
+1DRAF

Overflow Label

This feature allows you to customize the overflow-label that appears when there are more avatars than the maximum number set to display.

PropDefaultTypeDescription
overflow-label+${N}stringOverride the default overflow label.
Preview
Code
+6DRAF

Size and Square

PropDefaultTypeDescription
sizemdstringSets the size of the avatar.
square2.5emstringSets the avatar to a square shape with specified dimensions. This does not affect the size of the fallback value.

🚀 Adjust input size freely using any size, breakpoints (e.g., sm:sm, xs:lg), or states (e.g., hover:lg, focus:3xl).

Preview
Code
+6ESALEYPPDRAF
+6ESALEYPPDRAF
+6ESALEYPPDRAF

Customization

Similar to the size prop, any available props of the Avatar component can be directly passed to the AvatarGroup component. These props will then be automatically forwarded to the individual Avatar components within the group.

You can also use the una prop to add utility classes, refer to the Props and Presets sections for more information.

Preview
Code
+1DRAF

Slots

NamePropsDescription
default-The default slot for the AvatarGroup component.

Presets

shortcuts/avatar-group.ts
type AvatarGroupPrefix = 'avatar-group'

export const staticAvatarGroup: Record<`${AvatarGroupPrefix}-${string}` | AvatarGroupPrefix, string> = {
  'avatar-group': 'flex flex-row-reverse justify-end',
  'avatar-group-item': 'ring-0.125em ring-background -me-0.375em first:me-0',
  'avatar-group-count': 'text-0.875em',
}

export const dynamicAvatarGroup: [RegExp, (params: RegExpExecArray) => string][] = [
]

export const avatarGroup = [
  ...dynamicAvatarGroup,
  staticAvatarGroup,
]

Props

types/avatar-group.ts
import type { PrimitiveProps } from 'reka-ui'
import type { NAvatarProps } from './avatar'
/**
 * This extends the `NAvatarProps` interface.
 *
 * @see https://github.com/una-ui/una-ui/blob/main/packages/nuxt/src/runtime/types/avatar.ts
 */
export interface NAvatarGroupProps extends Omit<NAvatarProps, 'src' | 'alt' | 'label' | 'una'>, PrimitiveProps {
  /**
   * Set the maximum number of avatars to show.
   *
   */
  max?: number
  /**
   * Override the default overflow label.
   *
   * @default +${max}
   */
  overflowLabel?: string
  /**
   * `Una
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/avatar-group.ts
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/avatar.ts
   */
  una?: {
    avatarGroup?: string
    avatarGroupCount?: string
  } & NAvatarProps['una']
}

Components

AvatarGroup.vue
AvatarGroupDefault.vue
<script setup lang="ts">
import type { NAvatarGroupProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { Primitive } from 'reka-ui'
import { computed, h } from 'vue'
import { cn, omitProps } from '../../utils'
import Avatar from './avatar/Avatar.vue'

const props = withDefaults(defineProps<NAvatarGroupProps>(), {
  as: 'div',
})

const slots = defineSlots()

const max = computed(() => typeof props.max === 'string' ? Number.parseInt(props.max, 10) : props.max)

const children = computed(() => {
  let children = slots.default?.()
  if (children?.length) {
    children = children.flatMap((child: any) => {
      if (typeof child.type === 'symbol') {
        // `v-if="false"` or commented node
        if (typeof child.children === 'string') {
          return null
        }

        return child.children
      }

      return child
    }).filter(Boolean)
  }

  return children || []
})

// Calculate visible and hidden avatars without circular dependencies
const maxVisibleCount = computed(() => {
  if (!max.value || max.value <= 0) {
    return children.value.length
  }
  return Math.min(max.value, children.value.length)
})

const hiddenCount = computed(() => {
  if (!children.value.length) {
    return 0
  }
  return Math.max(0, children.value.length - maxVisibleCount.value)
})

const visibleAvatars = computed(() => {
  if (!children.value.length) {
    return []
  }

  // Take only the visible portion without modifying the original array
  return [...children.value].slice(0, maxVisibleCount.value).reverse()
})

const displayAvatars = computed(() => {
  const result = [...visibleAvatars.value]

  if (hiddenCount.value > 0 || props.overflowLabel) {
    const avatarProps = children.value.length > 0
      ? omitProps(children.value[0].props || {}, ['src', 'alt', 'label', 'icon'])
      : {}

    result.unshift(
      h(Avatar, {
        label: props.overflowLabel || `+${hiddenCount.value}`,
        class: cn(
          props.una?.avatarGroupCount,
        ),
        una: {
          avatarFallback: cn(
            'avatar-group-count',
            props.una?.avatarGroupCount,
            avatarProps.una?.avatarFallback,
          ),
          ...avatarProps.una,
        },
        ...avatarProps,
      }),
    )
  }

  return result
})

const rootProps = reactiveOmit(props, ['max', 'as', 'asChild', 'overflowLabel'])
</script>

<template>
  <Primitive
    :as
    :size
    :class="cn(
      'avatar-group',
      una?.avatarGroup,
    )"
    :as-child
  >
    <component
      :is="avatar"
      v-for="(avatar, count) in displayAvatars"
      v-bind="{ ...rootProps, ...avatar.props }"
      :key="count"
      :class="cn(
        'avatar-group-item',
        props.class,
      )"
    />
  </Primitive>
</template>