Primitives Layout and display
Avatar
An image with initials behind it, in three sizes, a square variant and an overlapping group. The browser loads and paints the image; the package stacks it over the initials and writes down which element carries the name. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - 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
On this page
Example
<div class="row">
<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>
<span data-slean="avatar" data-size="sm" role="img" aria-label="Rafael Duarte">
<span data-slean-part="fallback" aria-hidden="true">RD</span>
</span>
<span data-slean="avatar" data-size="lg">
<img src="/demo/avatar-jonas.svg" alt="Jonas Lindqvist" width="56" height="56" loading="lazy" />
<span data-slean-part="fallback" aria-hidden="true">JL</span>
</span>
<span data-slean="avatar" data-size="lg" data-variant="square" role="img" aria-label="Harbor Studio">
<span data-slean-part="fallback" aria-hidden="true">HS</span>
</span>
<div data-slean="avatar" data-variant="group" role="group" aria-label="Reviewers">
<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="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>
</div><script lang="ts">
import '@svelte-lean/styles/avatar.css';
</script>
<div class="row">
<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>
<span data-slean="avatar" data-size="sm" role="img" aria-label="Rafael Duarte">
<span data-slean-part="fallback" aria-hidden="true">RD</span>
</span>
<span data-slean="avatar" data-size="lg">
<img src="/demo/avatar-jonas.svg" alt="Jonas Lindqvist" width="56" height="56" loading="lazy" />
<span data-slean-part="fallback" aria-hidden="true">JL</span>
</span>
<span data-slean="avatar" data-size="lg" data-variant="square" role="img" aria-label="Harbor Studio">
<span data-slean-part="fallback" aria-hidden="true">HS</span>
</span>
<div data-slean="avatar" data-variant="group" role="group" aria-label="Reviewers">
<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="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>
</div>
<style>
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}
</style>Why this implementation exists
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.
The part a stylesheet cannot do is react to a failed image, so the contract says so and leaves it to the application, with the handling shown below.
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
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@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="sm", the default and "lg"; data-variant="square" is for teams and organisations. A group is one more avatar root with data-variant="group", role="group" and a label, holding the avatars; a count of the rest is an avatar whose initials are the count.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <span data-slean="avatar"> | – | yes | data-size="sm|md|lg", data-variant="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" data-variant="group" role="group"> | – | no | A wrapper root with an aria-label; its avatars overlap. |
Runtime profile
Tier 0: the avatar has no behavior module, the Vite plugin maps avatar to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript.
Accessibility contract
- The name is announced once: from the image’s
alt, or fromaria-labelon arole="img"root, never from the initials, which arearia-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.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab | anywhere | Passes the avatar by: it is not focusable unless wrapped in a link or button |
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.
Server rendering
Static HTML. The initials render on the server and stay visible until the image paints.
Before hydration
Nothing is attached. An image that loaded before hydration stays painted.
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 | 0.625rem | border-radius |
--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 (group, square); data-size (sm, lg).
Compatibility notes
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.
Examples
Missing and failed images
The initials show while an image loads because they sit behind it. An image that fails is
different: the browser paints its broken-image rendering in the image's box, over the initials
(Chrome draws an icon), and the stylesheet can only hide the alt text. The package ships no
error handler; the application decides. Rendering no <img> for an empty URL
and dropping it on the error event leaves the initials alone in both cases.
<script lang="ts">
// The application decides what a missing or failed image shows. Rendering no <img> for an
// empty URL, and dropping it on error, leaves the initials alone in both cases.
let { name, initials, src }: { name: string; initials: string; src?: string } = $props();
let failed = $state(false);
</script>
{#if src && !failed}
<span data-slean="avatar">
<img {src} alt={name} width="40" height="40" onerror={() => (failed = true)} />
<span data-slean-part="fallback" aria-hidden="true">{initials}</span>
</span>
{:else}
<span data-slean="avatar" role="img" aria-label={name}>
<span data-slean-part="fallback" aria-hidden="true">{initials}</span>
</span>
{/if}Decorative avatars
When the name is printed next to the avatar, the avatar repeats it. An empty alt keeps
it out of the accessibility tree so the name is read once.
<!-- The name is printed next to the avatar, so the avatar itself stays silent. -->
<p class="author">
<span data-slean="avatar" data-size="sm">
<img src="/demo/avatar-amara.svg" alt="" width="24" height="24" />
<span data-slean-part="fallback" aria-hidden="true">AO</span>
</span>
Amara Okafor
</p>Testing
apps/playground/tests/primitives/display.spec.tsPlaywright: open state, exclusive groups, stretched links, roles and names, carousel scrolling, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/avatar/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/avatar/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/avatar.cssthe optional stylesheet