# Avatar

> An image with initials behind it: circular, rounded or square, in three sizes and in groups.

Page: https://svelte-lean.vercel.app/primitives/avatar · Markdown: https://svelte-lean.vercel.app/primitives/avatar.md · Index: https://svelte-lean.vercel.app/llms.txt

An image of a person or team with initials behind it. Use it next to names, in lists of members and in comment threads.

```svelte
<script lang="ts">
	import '@svelte-lean/styles/avatar.css';
</script>

<span data-slean="avatar">
	<img src="/demo/avatar-amara.svg" alt="Amara Okafor" width="40" height="40" loading="lazy" />
	<span data-slean-part="fallback" aria-hidden="true">AO</span>
</span>
<span data-slean="avatar" role="img" aria-label="Mei Tanaka">
	<span data-slean-part="fallback" aria-hidden="true">MT</span>
</span>
```

The initials show until the image paints; an avatar without an image is named by aria-label.

## Examples

### Looks

```svelte
<script lang="ts">
	import '@svelte-lean/styles/avatar.css';

	const variants = ['circular', 'rounded', 'square'];
	const sizes = ['small', 'medium', 'large'];
	const colors = ['primary', 'secondary', 'success', 'warning'];
</script>

<!-- circular is the default variant, medium the default size; data-color tints the initials. -->
{#each variants as variant}
	<div class="row">
		{#each sizes as size}
			<span data-slean="avatar" data-variant={variant} data-size={size}>
				<img src="/demo/avatar-jonas.svg" alt="Jonas Lindqvist" width="40" height="40" />
				<span data-slean-part="fallback" aria-hidden="true">JL</span>
			</span>
		{/each}
		{#each colors as color}
			<span data-slean="avatar" data-variant={variant} data-color={color} role="img" aria-label="Harbor Studio">
				<span data-slean-part="fallback" aria-hidden="true">HS</span>
			</span>
		{/each}
	</div>
{/each}
```

circular (the default), rounded and square; small, medium and large; data-color tints the initials (neutral by default).

### Group

```svelte
<script lang="ts">
	import '@svelte-lean/styles/avatar.css';
</script>

<div data-slean="avatar" role="group" aria-label="Reviewers">
	<span data-slean="avatar">
		<img src="/demo/avatar-amara.svg" alt="Amara Okafor" width="40" height="40" />
		<span data-slean-part="fallback" aria-hidden="true">AO</span>
	</span>
	<span data-slean="avatar">
		<img src="/demo/avatar-jonas.svg" alt="Jonas Lindqvist" width="40" height="40" />
		<span data-slean-part="fallback" aria-hidden="true">JL</span>
	</span>
	<span data-slean="avatar" role="img" aria-label="Selin Aydın">
		<span data-slean-part="fallback" aria-hidden="true">SA</span>
	</span>
	<span data-slean="avatar" role="img" aria-label="Priya Raman">
		<span data-slean-part="fallback" aria-hidden="true">PR</span>
	</span>
	<span data-slean="avatar" role="img" aria-label="3 more reviewers">
		<span data-slean-part="fallback" aria-hidden="true">+3</span>
	</span>
</div>
```

An avatar root that holds avatar roots is a group (role="group" and a label); the last avatar counts the rest.

### With a name

```svelte
<script lang="ts">
	import '@svelte-lean/styles/avatar.css';
</script>

<!-- The name is printed next to the avatar, so the image has an empty alt and is read once. -->
<a class="person" href="/primitives/avatar">
	<span data-slean="avatar">
		<img src="/demo/avatar-amara.svg" alt="" width="40" height="40" />
		<span data-slean-part="fallback" aria-hidden="true">AO</span>
	</span>
	<span>
		<strong>Amara Okafor</strong>
		<small>amara@harbor.studio</small>
	</span>
</a>

<style>
	.person {
		display: inline-flex;
		align-items: center;
		gap: 0.75rem;
		color: inherit;
		text-decoration: none;
	}
	.person > span:last-child {
		display: grid;
	}
</style>
```

The printed name labels the link; the image takes an empty alt.

### Failed images

```svelte
<script lang="ts">
	import '@svelte-lean/styles/avatar.css';

	const people = [
		{ name: 'Amara Okafor', initials: 'AO', src: '/demo/avatar-amara.svg' },
		// An image that fails: a broken file stands in for a 404.
		{ name: 'Noor Haddad', initials: 'NH', src: 'data:image/svg+xml,broken' },
		{ name: 'Mei Tanaka', initials: 'MT', src: '' }
	];
	// The application decides what a failed image shows: drop the <img> on error, render none
	// for an empty URL, and the initials remain.
	let failed: Record<string, boolean> = $state({});
</script>

{#each people as person (person.name)}
	{#if person.src && !failed[person.name]}
		<span data-slean="avatar">
			<img
				src={person.src}
				alt={person.name}
				width="40"
				height="40"
				onerror={() => (failed[person.name] = true)}
			/>
			<span data-slean-part="fallback" aria-hidden="true">{person.initials}</span>
		</span>
	{:else}
		<span data-slean="avatar" role="img" aria-label={person.name}>
			<span data-slean-part="fallback" aria-hidden="true">{person.initials}</span>
		</span>
	{/if}
{/each}
```

The stylesheet cannot react to a failed image; the application drops it on error.

## Usage

Plain markup with a stylesheet: install `@svelte-lean/styles`. No JavaScript runs.

```bash
npm install @svelte-lean/styles
```

`stylesheets`

```ts
import '@svelte-lean/styles/avatar.css';
```

With an image, the `alt` text is the name and the initials are `aria-hidden`. Without one, the root takes `role="img"` and `aria-label`. When the name is printed next to the avatar, use `alt=""` and no label.

Sizes are `data-size="small"`, the default `medium` and `"large"`; `data-variant` is `circular` (the default), `rounded` (teams and organisations) or `square`. A group is one more avatar root around the avatars, with `role="group"` and a label; the stylesheet recognizes it by its structure.

## Anatomy

| Part | Element | data-slean-part | Required | Notes |
| --- | --- | --- | --- | --- |
| root | `<span data-slean="avatar">` | – | yes | data-size="small\|medium\|large", data-variant="circular\|rounded\|square". role="img" and aria-label when there is no image. |
| image | `<img alt="…">` | – | no | alt is the name, or empty when the name is printed next to the avatar. |
| fallback | `<span aria-hidden="true">` | `fallback` | no | Initials behind the image; visible until it paints or when there is none. |
| group | `<div data-slean="avatar" role="group">` | – | no | A wrapper root with an aria-label; its avatars overlap. |

## Keyboard

| Key | When | Result |
| --- | --- | --- |
| `Tab` | anywhere | Passes the avatar by: it is not focusable unless wrapped in a link or button |

## Styling

`avatar.css` draws the circle or rounded square, the three sizes with their initials, stacks the image over the initials in one grid cell, hides the alt text of a failed image, and overlaps the avatars of a group with a ring in the page color. The tokens it reads:

| Token | Default (light) | Applies to |
| --- | --- | --- |
| `--slean-radius-full` | `9999px` | border-radius |
| `--slean-muted` | `var(--slean-neutral-3)` | background |
| `--slean-fg-muted` | `var(--slean-neutral-11)` | color |
| `--slean-text-sm` | `0.875rem` | font-size |
| `--slean-font-weight-semibold` | `600` | font-weight |
| `--slean-radius-md` | `var(--slean-radius)` | border-radius |
| `--slean-color-soft` | `var(--slean-primary-soft)` | background |
| `--slean-color-soft-text` | `var(--slean-primary-soft-text)` | color |
| `--slean-text-xs` | `0.75rem` | font-size |
| `--slean-text-lg` | `1.125rem` | font-size |
| `--slean-bg` | `var(--slean-neutral-1)` | box-shadow |

Variant attributes: `data-variant` (`rounded`, `square`); `data-color`; `data-size` (`small`, `large`).

## Under the hood

Runtime and size

- **Tier**: 0 · Native
- **Behavior JS**: 0 B brotli · 0 B gzip · `fixtures/results.json (native-only)` · [method](https://svelte-lean.vercel.app/performance/methodology.md)
- **Platform features**: `<img>`, `alt`, `role="img"`, `CSS grid`
- **Shared listeners**: none
- **Per-instance listeners**: none
- **Lazy state**: none
- **Native base**: `<img> with initials behind it`

Tier 0: no behavior module. The Vite plugin maps `avatar` to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript.

Why this implementation

An avatar is an image, and the image element already loads lazily, reserves its box from `width` and `height`, and names itself from `alt`. What component libraries usually add is a loading state: a script that waits for the load event and swaps the initials for the image. Stacking both in one grid cell does the same with no script: until the image paints, its box is transparent and the initials show through; once it paints, it covers them.

A stylesheet cannot react to a failed image: the browser paints its broken-image rendering over the initials (Chrome draws an icon) and the stylesheet can only hide the alt text. The contract leaves that to the application, as the failed-image example shows.

### The browser owns

- loading and decoding the image, lazily with loading="lazy"
- the name from alt, or from aria-label on a role="img" root
- painting the image over the initials once it has loaded

### Svelte Lean owns

- avatar.css: the circle or square, three sizes, the initials under the image, the overlapping group
- the contract: which element carries the name in each form
- documentation

Accessibility

- The name is announced once: from the image’s `alt`, or from `aria-label` on a `role="img"` root, never from the initials, which are `aria-hidden`.
- A group is a named `role="group"`; the count avatar says what it counts (`3 more reviewers`).
- An avatar is not focusable. When it links to a profile, wrap it in the link and name the link.

Platform features

| Feature | Baseline | Outside the target |
| --- | --- | --- |
| `<img> with alt, width, height and loading="lazy"` | Widely available | Not applicable within the support policy |
| `CSS grid stacking` | Widely available | Not applicable within the support policy |

Without JavaScript

Fully functional: loading, painting and the stacking are the browser's and the stylesheet's. Dropping a failed image needs the application's script.

Server rendering and hydration

Static HTML. The initials render on the server and stay visible until the image paints.

Nothing is attached. An image that loaded before hydration stays painted.

Compatibility

Everything here is widely available. What a failed image paints differs between browsers: Chrome draws an icon over the initials, which the stylesheet does not hide.

Tests and source

- `apps/playground/tests/primitives/display.spec.ts` Playwright: open state, exclusive groups, stretched links, roles and names, carousel scrolling, axe
- `packages/styles/tests` static checks of the stylesheet (layers, tokens, specificity, dark parity)

- `packages/primitives/src/avatar/contract.md` the contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behavior
- `packages/primitives/src/avatar/contract.ts` typed constants: name, tier, base, parts, options, events
- `packages/styles/css/avatar.css` the optional stylesheet
