Skip to content
Velocity X Docs
← All components

Avatar + Avatar Group

Free · MIT

Single avatar or an overlapping stack with a plus-N overflow chip, falling back to deterministic initials-over-tint when no image is given.

Category
Core UI
Framework
astro
Version
1.0.0
Peer deps
none

Install

bash
velocityx add avatar-group

Free and MIT licensed — no account needed. The file lands under src/components/velocity/avatar-group/ by default (override with --dest), and it's yours to edit from the moment it's written.

Demo

View the live demo on the showcase site ↗

Source

Pulled live from the registry — this is the exact file velocityx add avatar-group writes into your project right now.

components/velocity/avatar-group/AvatarGroup.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * AvatarGroup — single avatar or an overlapping stack with a "+N" overflow
 * chip. Falls back to initials over a deterministic brand-tint background
 * when no image is given (no external avatar service dependency).
 *
 * Usage:
 *   <AvatarGroup people={[{ name: 'Aiden Wood', src: '/a.jpg' }, { name: 'Elijah' }]} max={4} />
 */
export interface Person {
  name: string;
  src?: string;
}
export interface Props {
  people: Person[];
  max?: number;
  size?: 'sm' | 'md' | 'lg';
}
const { people, max = 5, size = 'md' } = Astro.props;

const SIZES = { sm: 'h-7 w-7 text-[10px]', md: 'h-9 w-9 text-xs', lg: 'h-12 w-12 text-sm' } as const;
const RING = 'ring-2 ring-white dark:ring-secondary-950';

const TINTS = [
  'bg-primary-100 text-primary-700 dark:bg-primary-500/20 dark:text-primary-300',
  'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300',
  'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-300',
  'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300',
  'bg-secondary-200 text-secondary-700 dark:bg-secondary-700 dark:text-secondary-200',
];

function initials(name: string) {
  return name.split(' ').filter(Boolean).slice(0, 2).map((n) => n[0]).join('').toUpperCase();
}
function tintFor(name: string) {
  let hash = 0;
  for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) >>> 0;
  return TINTS[hash % TINTS.length];
}

const visible = people.slice(0, max);
const overflow = people.length - visible.length;
---

<div class="flex items-center -space-x-2.5" role="group" aria-label={`${people.length} people`}>
  {visible.map((p) => (
    <span class:list={['relative inline-flex shrink-0 items-center justify-center rounded-full overflow-hidden font-semibold', SIZES[size], RING, !p.src && tintFor(p.name)]} title={p.name}>
      {p.src ? (
        <img src={p.src} alt={p.name} class="h-full w-full object-cover" />
      ) : (
        <span aria-hidden="true">{initials(p.name)}</span>
      )}
      <span class="sr-only">{p.name}</span>
    </span>
  ))}
  {overflow > 0 && (
    <span
      class:list={['relative inline-flex shrink-0 items-center justify-center rounded-full bg-secondary-100 text-secondary-600 dark:bg-secondary-800 dark:text-secondary-300 font-semibold', SIZES[size], RING]}
      title={`${overflow} more`}
    >
      +{overflow}
    </span>
  )}
</div>

Theming

Uses the shared primary-* / accent-* scale — see Theming if colours look unstyled after install.