Avatar
User avatars — a single image/initials avatar and an overlapping avatar group with an overflow count.
Avatar — User avatar with image and fallback rendering, size and shape variants.
Preview
<div class="flex items-center gap-3">
<x-ui.avatar size="sm" fallback="JD" alt="{{ __('Jane Doe') }}" />
<x-ui.avatar fallback="AB" alt="{{ __('Alex Brown') }}" />
<x-ui.avatar size="lg" fallback="CD" alt="{{ __('Casey Doyle') }}" />
</div>
Size options
Shape options
Tone options
Installation
php artisan ui:add avatar
Registry contract
php artisan ui:add avatar
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/avatar.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Avatar (`avatar`)
User avatar with image and fallback rendering, size and shape variants.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add avatar
```
## Usage
```blade
<div class="flex items-center gap-3">
<x-ui.avatar size="sm" fallback="JD" alt="{{ __('Jane Doe') }}" />
<x-ui.avatar fallback="AB" alt="{{ __('Alex Brown') }}" />
<x-ui.avatar size="lg" fallback="CD" alt="{{ __('Casey Doyle') }}" />
</div>
```
## Props
- `src` (mixed|null, default `null`) — Image URL; when omitted the fallback content renders instead.
- `alt` (string, default ``) — Accessible name for the image; an empty string marks the avatar as decorative.
- `size` (md|xs|sm|lg|xl, default `md`) — Square size token from xs to xl.
- `shape` (string, default `null`) — Outline shape: circle, rounded, square, or tile (size-scaled rounded square). When omitted it follows kind (company: tile) and otherwise circle.
- `fallback` (mixed|null, default `null`) — Content shown when src is empty, typically initials.
- `name` (string|null, default `null`) — Opt-in name mode: derives 1-2 initials (first and last word, Unicode-aware, mb_ functions) and a tint the name always hashes to; labels the avatar with the name unless alt or decorative says otherwise; a src that fails to load shows the initials.
- `kind` (person|company|null, default `null`) — Default outline: person is a circle, company a rounded square (shape tile, radius scaled to the size). An explicit shape wins.
- `tone` (auto|neutral, default `auto`) — auto tints a named avatar with one of five chart-hue pairs (bg-chart-N/20 text-foreground); neutral keeps bg-muted. Without name the avatar is always neutral.
- `decorative` (bool, default `false`) — Hides the avatar from assistive tech (aria-hidden) even when alt or name is set; use it when a visible name sits next to the avatar.
## Use when
- Use to visually represent people or entities in collaborative or social contexts.
- Representing a single person or entity with a photo, falling back to initials or an icon when no image is available.
- Sizing and shaping a face consistently across lists, headers, and cards.
## Avoid when
- Do not rely on avatars alone when names or roles are needed for disambiguation.
- Showing several people at once; use avatar-group so the faces overlap and collapse into a count.
## Anti-patterns
- Relying on an avatar alone for identity
## Rules
- Use the `<brok:avatar>` tag (or `<x-ui.avatar>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/avatar
- Registry JSON (files, props, contract): https://brokui.dev/r/open/avatar.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
<div class="flex items-center">
<div class="flex -space-x-3 rtl:space-x-reverse">
<x-ui.avatar fallback="SR" alt="{{ __('Sofia Rodrigues') }}" class="ring-2 ring-background" />
<x-ui.avatar fallback="LC" alt="{{ __('Liam Chen') }}" class="ring-2 ring-background" />
<x-ui.avatar fallback="AM" alt="{{ __('Amir Malik') }}" class="ring-2 ring-background" />
<x-ui.avatar fallback="JD" alt="{{ __('Jane Doe') }}" class="ring-2 ring-background" />
<x-ui.avatar fallback="+5" alt="{{ __('5 more members') }}" class="ring-2 ring-background bg-accent text-accent-foreground" />
</div>
</div>
Image Fallback
<div class="flex flex-wrap items-center gap-3">
<x-ui.avatar kind="company" name="Brok UI" src="/favicon-96x96.png" data-test="loaded" />
<x-ui.avatar kind="company" name="Missing Logo Ltd" src="/images/missing-logo.png" data-test="broken-company" />
<x-ui.avatar name="Ava Chen" src="/images/missing-photo.jpg" data-test="broken-person" />
<x-ui.avatar name="Ava Chen" src="/images/missing-photo.jpg" decorative data-test="decorative" />
<x-ui.avatar name="Ava Chen" data-test="reference" />
</div>
<div class="flex flex-wrap items-center gap-4">
@foreach (['xs', 'sm', 'md', 'lg', 'xl'] as $size)
<div class="flex items-center gap-2">
<x-ui.avatar :size="$size" kind="person" name="Ava Chen" />
<x-ui.avatar :size="$size" kind="company" name="Northwind Traders" />
</div>
@endforeach
</div>
Long Content
<div class="flex max-w-xs flex-col gap-3">
<div class="flex min-w-0 items-center gap-3">
<x-ui.avatar kind="company" name="The International Consolidated Airlines Group of Very Long Names" decorative />
<span class="min-w-0 truncate text-sm text-foreground">{{ __('The International Consolidated Airlines Group of Very Long Names') }}</span>
</div>
<div class="flex min-w-0 items-center gap-3">
<x-ui.avatar name="Maximiliane-Theodora von Hohenzollern-Sigmaringen" decorative />
<span class="min-w-0 truncate text-sm text-foreground">{{ __('Maximiliane-Theodora von Hohenzollern-Sigmaringen') }}</span>
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
<x-ui.avatar name="Ava Chen" />
<x-ui.avatar name="Bram van Dijk" />
<x-ui.avatar name="Émile Zola" />
<x-ui.avatar name="Łukasz" />
<x-ui.avatar name="山田 太郎" />
<x-ui.avatar name="Dev Rao" tone="neutral" />
</div>
<div class="flex flex-wrap items-center gap-3">
<x-ui.avatar shape="circle" fallback="CI" alt="{{ __('Circle') }}" />
<x-ui.avatar shape="rounded" fallback="RO" alt="{{ __('Rounded') }}" />
<x-ui.avatar shape="square" fallback="SQ" alt="{{ __('Square') }}" />
</div>
<div class="flex flex-wrap items-center gap-3">
<x-ui.avatar size="xs" fallback="XS" alt="{{ __('Extra small') }}" />
<x-ui.avatar size="sm" fallback="SM" alt="{{ __('Small') }}" />
<x-ui.avatar size="md" fallback="MD" alt="{{ __('Medium') }}" />
<x-ui.avatar size="lg" fallback="LG" alt="{{ __('Large') }}" />
<x-ui.avatar size="xl" fallback="XL" alt="{{ __('Extra large') }}" />
</div>
<div class="flex flex-wrap items-center gap-4">
<span class="relative inline-flex">
<x-ui.avatar fallback="ON" alt="{{ __('Online user') }}" />
<span class="absolute bottom-0 end-0 size-3 rounded-full bg-success ring-2 ring-background" aria-hidden="true"></span>
<span class="sr-only">{{ __('Online') }}</span>
</span>
<span class="relative inline-flex">
<x-ui.avatar fallback="AW" alt="{{ __('Away user') }}" />
<span class="absolute bottom-0 end-0 size-3 rounded-full bg-warning ring-2 ring-background" aria-hidden="true"></span>
<span class="sr-only">{{ __('Away') }}</span>
</span>
<span class="relative inline-flex">
<x-ui.avatar fallback="BS" alt="{{ __('Busy user') }}" />
<span class="absolute bottom-0 end-0 size-3 rounded-full bg-destructive ring-2 ring-background" aria-hidden="true"></span>
<span class="sr-only">{{ __('Busy') }}</span>
</span>
<span class="relative inline-flex">
<x-ui.avatar fallback="OF" alt="{{ __('Offline user') }}" />
<span class="absolute bottom-0 end-0 size-3 rounded-full bg-muted-foreground ring-2 ring-background" aria-hidden="true"></span>
<span class="sr-only">{{ __('Offline') }}</span>
</span>
</div>
With Badge
<div class="flex flex-wrap items-center gap-4">
<span class="relative inline-flex">
<x-ui.avatar size="lg" fallback="SR" alt="{{ __('Sofia Rodrigues') }}" />
<span class="absolute -bottom-1 -end-1 inline-flex size-6 items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background">
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5" />
</svg>
</span>
<span class="sr-only">{{ __('Verified') }}</span>
</span>
<span class="relative inline-flex">
<x-ui.avatar size="lg" fallback="AM" alt="{{ __('Amir Malik') }}" />
{{-- A label badge sits half below the avatar's edge, so the initials stay readable. --}}
<x-ui.badge variant="outline" size="sm" class="absolute bottom-0 end-1/2 translate-x-1/2 translate-y-1/2 rtl:-translate-x-1/2 bg-background">
{{ __('Pro') }}
</x-ui.badge>
</span>
</div>
With Icon
<div class="flex flex-wrap items-center gap-3">
<x-ui.avatar size="sm" alt="{{ __('User placeholder') }}">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</x-ui.avatar>
<x-ui.avatar alt="{{ __('User placeholder') }}">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</x-ui.avatar>
<x-ui.avatar size="lg" shape="rounded" alt="{{ __('User placeholder') }}">
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</x-ui.avatar>
</div>
With Name
<div class="flex items-center gap-3">
<x-ui.avatar size="lg" fallback="SR" alt="{{ __('Sofia Rodrigues') }}" />
<div class="flex flex-col">
<span class="text-sm font-medium text-foreground">{{ __('Sofia Rodrigues') }}</span>
<span class="text-xs text-muted-foreground">sofia@example.com</span>
</div>
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| src | mixed | null | null | Image URL; when omitted the fallback content renders instead. |
| alt | string | Accessible name for the image; an empty string marks the avatar as decorative. | |
| size | md | xs | sm | lg | xl | md | Square size token from xs to xl. |
| shape | string | null | Outline shape: circle, rounded, square, or tile (size-scaled rounded square). When omitted it follows kind (company: tile) and otherwise circle. |
| fallback | mixed | null | null | Content shown when src is empty, typically initials. |
| name | string | null | null | Opt-in name mode: derives 1-2 initials (first and last word, Unicode-aware, mb_ functions) and a tint the name always hashes to; labels the avatar with the name unless alt or decorative says otherwise; a src that fails to load shows the initials. |
| kind | person | company | null | null | Default outline: person is a circle, company a rounded square (shape tile, radius scaled to the size). An explicit shape wins. |
| tone | auto | neutral | auto | auto tints a named avatar with one of five chart-hue pairs (bg-chart-N/20 text-foreground); neutral keeps bg-muted. Without name the avatar is always neutral. |
| decorative | bool | false | Hides the avatar from assistive tech (aria-hidden) even when alt or name is set; use it when a visible name sits next to the avatar. |
Slots
default—
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Renders the image when src is set; otherwise shows the fallback content, typically initials, inside a muted circle.
- alt is announced when present; when alt is empty the fallback content is marked decorative and hidden from assistive tech.
- Name mode (name set) renders the root as role="img" with aria-label (alt, else name) or aria-hidden when decorative; the image is layered on top with alt="" and its ::after pseudo-element shows the initials when the image fails, so no broken-image icon appears and no JavaScript is needed (Chromium and Firefox; Safari shows the tint without initials).
- The tint index is crc32 of the lower-cased name modulo five, so a name keeps its colour across pages and requests; data-tone exposes it as tint-1..tint-5 or neutral.
- Without name, kind or decorative the output is unchanged from 1.1.x.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Represent a person or entity visually.
Use when
- Use to visually represent people or entities in collaborative or social contexts.
- Representing a single person or entity with a photo, falling back to initials or an icon when no image is available.
- Sizing and shaping a face consistently across lists, headers, and cards.
Avoid when
- Do not rely on avatars alone when names or roles are needed for disambiguation.
- Showing several people at once; use avatar-group so the faces overlap and collapse into a count.
Use instead
- Name and role text
Anti-patterns
- Relying on an avatar alone for identity
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- Pass alt with the person's name whenever the avatar conveys identity; leave it empty only for a purely decorative avatar.
- Name mode is labelled by default (role="img", aria-label = alt or name); pass decorative when the name is already visible beside it, so it is not announced twice.
- Monogram text is the foreground token on a 20% chart tint, above 4.5:1 in light and dark.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'src' => null,
'alt' => '',
'size' => 'md',
'shape' => null,
'fallback' => null,
'name' => null,
'kind' => null,
'tone' => 'auto',
'decorative' => false,
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$sizes = [
'xs' => 'size-6 text-[0.625rem]',
'sm' => 'size-8 text-xs',
'md' => 'size-10 text-sm',
'lg' => 'size-12 text-base',
'xl' => 'size-16 text-lg',
];
$size = isset($sizes[$size]) ? $size : 'md';
// `kind` picks the default outline: a person is a circle, a company a
// rounded square (`tile`). An explicit `shape` always wins.
$kind = in_array($kind, ['person', 'company'], true) ? $kind : null;
$shape = $shape ?? ($kind === 'company' ? 'tile' : 'circle');
$shapes = [
'circle' => 'rounded-full',
'rounded' => 'rounded-lg',
'square' => 'rounded-none',
'tile' => $styles['avatar']['tileRadius'][$size],
];
// Name mode: initials (first and last word, 1-2 letters) and a tint the
// name always hashes to. Without a name the avatar renders as before.
$name = filled($name) ? trim((string) $name) : null;
$monogram = null;
$toneIndex = null;
$toneClass = 'bg-muted text-muted-foreground';
if ($name !== null) {
$letters = [];
foreach (preg_split('/\s+/u', $name, -1, PREG_SPLIT_NO_EMPTY) ?: [] as $word) {
if (preg_match('/[\p{L}\p{N}]/u', $word, $match) === 1) {
$letters[] = $match[0];
}
}
$picked = count($letters) > 1 ? [$letters[0], $letters[count($letters) - 1]] : $letters;
$monogram = mb_strtoupper(implode('', $picked));
if ($tone !== 'neutral') {
$palette = $styles['avatar']['tones'];
$toneIndex = crc32(mb_strtolower($name)) % count($palette);
$toneClass = $palette[$toneIndex];
}
}
$imageFallbackClass = '';
if ($name !== null) {
$imageFallbackClass = $styles['avatar']['imageFallbackBase'].' '
.($toneIndex === null ? $styles['avatar']['imageFallbackNeutral'] : $styles['avatar']['imageFallback'][$toneIndex]);
}
$classes = trim('relative inline-flex shrink-0 items-center justify-center overflow-hidden '
.$toneClass.' '
.($shapes[$shape] ?? $shapes['circle']).' '
.$sizes[$size]);
$label = $decorative ? '' : ($alt !== '' ? (string) $alt : (string) $name);
$content = $fallback ?? ($slot->isEmpty() ? $monogram : $slot);
$imageFallback = is_string($fallback) && $fallback !== '' ? $fallback : (string) $monogram;
@endphp
@if ($name === null)
<span
data-slot="avatar"
data-size="{{ $size }}"
data-shape="{{ $shape }}"
@if ($kind) data-kind="{{ $kind }}" @endif
@if ($decorative) aria-hidden="true" @endif
{{ $attributes->merge(['class' => $classes]) }}
>
@if ($src)
<img src="{{ $src }}" alt="{{ $decorative ? '' : $alt }}" class="size-full object-cover" />
@else
<span class="font-medium" aria-hidden="{{ $alt === '' || $decorative ? 'true' : 'false' }}">{{ $fallback ?? $slot }}</span>
@if ($alt !== '' && ! $decorative)
<span class="sr-only">{{ $alt }}</span>
@endif
@endif
</span>
@else
{{-- Name mode: the root carries the accessible name (role="img") or is hidden
when decorative. A broken image paints its pseudo-elements in Chromium
and Firefox: ::before covers the broken-image glyph, ::after shows the
tinted initials, so a failed
load never shows a broken-image icon and needs no JavaScript. --}}
<span
data-slot="avatar"
data-size="{{ $size }}"
data-shape="{{ $shape }}"
@if ($kind) data-kind="{{ $kind }}" @endif
data-tone="{{ $toneIndex === null ? 'neutral' : 'tint-'.($toneIndex + 1) }}"
@if ($label !== '') role="img" aria-label="{{ $label }}" @else aria-hidden="true" @endif
{{ $attributes->merge(['class' => $classes]) }}
>
@if ($src)
<img
data-slot="avatar-image"
src="{{ $src }}"
alt=""
data-fallback="{{ $imageFallback }}"
class="absolute inset-0 size-full object-cover {{ $imageFallbackClass }}"
/>
@else
<span data-slot="avatar-fallback" class="font-medium" aria-hidden="true">{{ $content }}</span>
@endif
</span>
@endif
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.2.0 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0