Avatar
An image of a person, with a fallback while it loads or if it can't.
Melt UI Avatar
JB CN SM LG
<script lang="ts">
import * as Avatar from '#lib/components/ui/avatar/index.js';
</script>
<div class="row">
<Avatar.Root>
<Avatar.Image src="/avatar.svg" alt="Profile picture" />
<Avatar.Fallback>JB</Avatar.Fallback>
</Avatar.Root>
<Avatar.Root>
<Avatar.Image src="/missing.png" alt="Missing picture" />
<Avatar.Fallback>CN</Avatar.Fallback>
</Avatar.Root>
<Avatar.Root size="sm"><Avatar.Fallback>SM</Avatar.Fallback></Avatar.Root>
<Avatar.Root size="lg"><Avatar.Fallback>LG</Avatar.Fallback></Avatar.Root>
</div>
<style>
.row {
display: flex;
align-items: center;
gap: var(--dui-space-3);
}
</style>
Group
AD BO CY
<script lang="ts">
import * as Avatar from '#lib/components/ui/avatar/index.js';
</script>
<Avatar.Group>
<Avatar.Root>
<Avatar.Image src="/avatar.svg" alt="Ada" />
<Avatar.Fallback>AD</Avatar.Fallback>
</Avatar.Root>
<Avatar.Root><Avatar.Fallback>BO</Avatar.Fallback></Avatar.Root>
<Avatar.Root><Avatar.Fallback>CY</Avatar.Fallback></Avatar.Root>
</Avatar.Group>
Installation
npx distill-ui add avatarIt installs @floating-ui/domandmelt if you don't have them yet.
- Set up the tokens once, if you haven't: see manual installation.
- Install the packages it needs:
npm install @floating-ui/dom@^1.6.0 melt@0.44.0 - Copy these files into your project:
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { getAvatarContext } from './context.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: HTMLAttributes<HTMLSpanElement> & { ref?: HTMLSpanElement | null } = $props();
const ctx = getAvatarContext();
</script>
<span {...restProps} {...ctx.avatar.fallback} bind:this={ref} class={['fallback', className]}>
{@render children?.()}
</span>
<!--
Options you can set from a parent or on the avatar fallback itself:
--dui-avatar-fallback-bg
-->
<style>
.fallback {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: inherit;
background: var(--dui-avatar-fallback-bg, var(--dui-color-muted));
/* Pulled toward the foreground so initials pass AA contrast on the muted fill. */
color: var(
--dui-avatar-fallback-fg,
color-mix(in oklch, var(--dui-color-muted-foreground) 70%, var(--dui-color-foreground))
);
}
</style>
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: HTMLAttributes<HTMLDivElement> & { ref?: HTMLDivElement | null } = $props();
</script>
<div {...restProps} bind:this={ref} class={['group', className]}>
{@render children?.()}
</div>
<style>
.group {
/* Read by each Avatar inside: overlap them and ring each one in the page color. */
--avatar-overlap: calc(var(--dui-space-2) * -1);
--avatar-ring-width: 2px;
display: flex;
}
</style>
<script lang="ts">
import type { HTMLImgAttributes } from 'svelte/elements';
import { getAvatarContext } from './context.js';
let {
ref = $bindable(null),
src,
alt,
style,
class: className,
...restProps
}: HTMLImgAttributes & { ref?: HTMLImageElement | null; src: string; alt: string } = $props();
const ctx = getAvatarContext();
// Set right away too, not only in the effect: effects don't run during server rendering,
// and the browser keeps whatever `src` the server sent.
// svelte-ignore state_referenced_locally
ctx.src = src;
$effect.pre(() => {
ctx.src = src;
});
// Melt listens for the image's load and error events. When the page is server-rendered,
// the image can finish before those listeners attach, so check its state once mounted.
$effect(() => {
if (!ref?.complete || !src) return;
if (ref.naturalWidth > 0) return ctx.avatar.image.onload();
ctx.avatar.image.onerror();
});
</script>
<img
{...restProps}
{...ctx.avatar.image}
bind:this={ref}
{alt}
style="{ctx.avatar.image.style}; {style ?? ''}"
class={['image', className]}
/>
<style>
.image {
width: 100%;
height: 100%;
aspect-ratio: 1;
border-radius: inherit;
object-fit: cover;
}
</style>
<script lang="ts">
import { Avatar } from 'melt/builders';
import type { HTMLAttributes } from 'svelte/elements';
import { setAvatarContext } from './context.js';
let {
ref = $bindable(null),
size = 'md',
class: className,
children,
...restProps
}: HTMLAttributes<HTMLSpanElement> & {
ref?: HTMLSpanElement | null;
size?: 'sm' | 'md' | 'lg';
} = $props();
const ctx: { avatar: Avatar; src: string | undefined } = $state({
avatar: undefined as unknown as Avatar,
src: undefined
});
ctx.avatar = new Avatar({ src: () => ctx.src });
setAvatarContext(ctx);
</script>
<span {...restProps} bind:this={ref} class={['avatar', className]} data-size={size}>
{@render children?.()}
</span>
<!--
Options you can set from a parent or on the avatar itself:
--dui-avatar-size, --dui-avatar-radius
-->
<style>
.avatar {
position: relative;
display: flex;
flex-shrink: 0;
width: var(--dui-avatar-size, 2rem);
height: var(--dui-avatar-size, 2rem);
/* Set by Avatar.Group so stacked avatars overlap with a gap ring. */
margin-inline-start: var(--avatar-overlap, 0);
border-radius: var(--dui-avatar-radius, var(--dui-radius-full));
box-shadow: 0 0 0 var(--avatar-ring-width, 0) var(--dui-color-background);
font-size: var(--dui-text-sm);
user-select: none;
&:first-child {
margin-inline-start: 0;
}
/* A faint edge so light images don't blend into the page. */
&::after {
content: '';
position: absolute;
inset: 0;
border: 1px solid color-mix(in oklch, var(--dui-color-foreground) 10%, transparent);
border-radius: inherit;
pointer-events: none;
}
}
.avatar[data-size='sm'] {
width: var(--dui-avatar-size, 1.5rem);
height: var(--dui-avatar-size, 1.5rem);
font-size: var(--dui-text-xs);
}
.avatar[data-size='lg'] {
width: var(--dui-avatar-size, 2.5rem);
height: var(--dui-avatar-size, 2.5rem);
}
</style>
import { getContext, setContext } from 'svelte';
import type { Avatar } from 'melt/builders';
// The Melt builder tracks whether the image loaded, so Fallback knows when to show.
// Image passes its `src` up through `src` so the builder can watch it.
type AvatarContext = { avatar: Avatar; src: string | undefined };
const key = Symbol('distill-avatar');
export const setAvatarContext = (ctx: AvatarContext) => setContext(key, ctx);
export const getAvatarContext = () => getContext<AvatarContext>(key);
import Fallback from './avatar-fallback.svelte';
import Group from './avatar-group.svelte';
import Image from './avatar-image.svelte';
import Root from './avatar.svelte';
export { Root, Image, Fallback, Group };
Customizing
Set any of these on the component or on a parent element. See Customizing for how they work.
| Custom property | Default |
|---|---|
--dui-avatar-fallback-bg | var(--dui-color-muted) |
--dui-avatar-size | 2rem |
--dui-avatar-radius | var(--dui-radius-full) |