Avatar
A person's image with an initials fallback, wired in one line.
pnpm dlx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-avatar.jsonnpx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-avatar.jsonbunx --bun shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-avatar.jsonyarn dlx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-avatar.jsonRaw shadcn asks for three components and a hand-written fallback every single time:
<Avatar>
<AvatarImage src={user.photo} alt={user.name} />
<AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>That last line is the problem. It is rewritten in every project, and it is wrong in most of them: slice(0, 2) on Elena van der Meer gives EL, not EM.
<script lang="ts">
import Avatar from '#lib/components/coral/kit/avatar/avatar.svelte';
</script>
<Avatar src="https://github.com/shadcn.png" name="Shad CN" />
<Avatar name="Elena van der Meer" />
<Avatar name="Kenji" />What Coral adds
- Correct initials, not
slice(0, 2). First letter of the first word, plus first letter of the last one: "Elena van der Meer" givesEM, notEL. - One accessible label, never doubled. The image's
altand the fallback's hidden label are the same text, and only one of the two is ever in the accessibility tree at a time. - An empty state without a placeholder nobody asked for. No
nameand nofallbackis an empty circle, not text made up to fill it. - The pieces stay reachable.
childrenrenders alongside the image, andAvatarGroupstill wraps a Coral avatar unchanged.
Import
<script lang="ts">
import Avatar from '#lib/components/coral/kit/avatar/avatar.svelte';
</script>Props
Everything the shadcn root accepts stays available: size, class, delayMs, loadingStatus, ref and any div attribute. On top of that:
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | - | Image URL. Absent or failed to load, the fallback shows instead. |
alt | string | name | Alt text for the image. Falls back to '', i.e. decorative. |
name | string | - | Person's name. Derives the initials and acts as the default alt. |
fallback | string | Snippet | - | Replaces the derived initials. Text, or a snippet for full control. |
children | Snippet | - | Extra content inside the root, alongside the image - e.g. a badge. |
Fallback
The fallback is shown when there is no src, and when the image fails to load. Initials come
from name: first letter of the first word, plus first letter of the last word when there is more
than one. Single-word names give a single letter.
An empty or missing name produces an empty string rather than a placeholder. The caller decides
what an unknown person looks like, and gets an empty circle until it says so.
<script lang="ts">
import Avatar from '#lib/components/coral/kit/avatar/avatar.svelte';
</script>
<!-- No image at all -->
<Avatar name="Amara Diallo" />
<!-- Image that will fail to load -->
<Avatar src="https://example.invalid/missing.png" name="Amara Diallo" />
<!-- Explicit fallback wins over the derived initials -->
<Avatar name="Amara Diallo" fallback="AR" />
<!-- No name and no fallback: an empty circle, not a placeholder nobody asked for -->
<Avatar />Accessibility
An avatar is called the same thing whether or not the photo loads. name becomes the image's alt, and (when the fallback is showing instead) a visually hidden label, so a screen reader
reads Elena van der Meer rather than the letters E M.
Only one of the two is ever in the accessibility tree: the fallback is display: none while the
image is showing, so the name is never announced twice.
| Given | A screen reader reads | On screen |
|---|---|---|
name | the name | initials |
name + a broken src | the name | initials |
name + a working src | the name (via alt) | the photo |
name + alt="" | nothing | initials |
fallback with no name | the fallback text | that text |
| nothing | nothing | empty |
alt="" means here what it means everywhere else: this avatar is decorative, the name is already
in the text next to it. Use it in a table row or a list item that already names the person, and the
same name will not be read twice.
Sizes
size is forwarded straight to the shadcn root. Coral adds nothing here, and removes nothing.
<script lang="ts">
import Avatar from '#lib/components/coral/kit/avatar/avatar.svelte';
</script>
<Avatar size="sm" src="https://github.com/shadcn.png" name="Shad CN" />
<Avatar size="default" src="https://github.com/shadcn.png" name="Shad CN" />
<Avatar size="lg" src="https://github.com/shadcn.png" name="Shad CN" />
<Avatar size="sm" name="Amara Diallo" />
<Avatar size="default" name="Amara Diallo" />
<Avatar size="lg" name="Amara Diallo" />Custom fallback
When initials are not the right answer (an unauthenticated user, a machine account), pass a snippet instead of a string.
<script lang="ts">
import UserIcon from '@lucide/svelte/icons/user';
import Avatar from '#lib/components/coral/kit/avatar/avatar.svelte';
</script>
<Avatar>
{#snippet fallback()}
<UserIcon class="size-4" />
{/snippet}
</Avatar>
<Avatar size="lg">
{#snippet fallback()}
<UserIcon class="size-5" />
{/snippet}
</Avatar>Escaping the flat API
The pieces stay reachable. children renders inside the root, next to the image, so anything the
primitive supports still works without dropping Coral.
<script lang="ts">
import CheckIcon from '@lucide/svelte/icons/check';
import Avatar from '#lib/components/coral/kit/avatar/avatar.svelte';
import { AvatarBadge } from '#lib/components/ui/avatar/index.js';
</script>
<!-- `children` renders inside the root, alongside the image - the primitive stays reachable -->
<Avatar src="https://github.com/shadcn.png" name="Shad CN" size="lg">
<AvatarBadge />
</Avatar>
<Avatar name="Amara Diallo" size="lg">
<AvatarBadge>
<CheckIcon />
</AvatarBadge>
</Avatar>The same goes for grouping: AvatarGroup comes from shadcn and Coral avatars sit inside it
unchanged.
<script lang="ts">
import Avatar from '#lib/components/coral/kit/avatar/avatar.svelte';
import { AvatarGroup, AvatarGroupCount } from '#lib/components/ui/avatar/index.js';
const team = ['Amara Diallo', 'Wei Zhang', 'Sofia Rossi', "Liam O'Connor"];
</script>
<AvatarGroup>
{#each team as name (name)}
<Avatar {name} />
{/each}
<AvatarGroupCount>+3</AvatarGroupCount>
</AvatarGroup>Why flat props
Avatar has a defensible canonical case: a person, maybe a photo, initials when there is no photo. The parts do not vary independently, so composition would be ceremony. This is the shape the conventions call for: flat props, and the pieces still reachable when the rare case shows up.
initials()
The derivation lives in kit/avatar/initials.ts and is exported on its own, so a project that
needs the same initials somewhere else (a mention chip, an export, a PDF) does not re-implement
it.
import { initials } from '#lib/components/coral/kit/avatar/initials.js';
initials('Elena van der Meer'); // 'EM'
initials('Kenji'); // 'K'
initials(' '); // ''
initials(undefined); // ''It stays inside the component's folder rather than in lib/ because it has exactly one consumer
in Coral today. It moves the day a second component needs it.