Avatar
avatar.svelteA square avatar tile with an image or placeholder, optionally a link.
Demo
Usage
001 <script lang="ts">
002 import Avatar from '$lib/components/avatar.svelte';
003 </script>
004
005 <div class="flex flex-row flex-wrap items-start gap-4">
006 <Avatar src="https://avatars.githubusercontent.com/u/9919?v=4" />
007 <Avatar />
008 <Avatar href="https://example.com">Ada Lovelace</Avatar>
009 </div>
010
avatar.svelte
001 <script lang="ts" module>
002 import type { HTMLAnchorAttributes, HTMLAttributes } from 'svelte/elements';
003
004 /**
005 * A square avatar tile. Renders a background image when `src` is set, else a
006 * placeholder block. Renders `<a>` when `href` is set, `<figure>` otherwise —
007 * `href` discriminates the allowed attributes. When `children` is provided,
008 * the avatar is wrapped in a flex row with the children beside it.
009 */
010 export type AvatarProps =
011 | (HTMLAttributes<HTMLDivElement> & {
012 ref?: HTMLElement | null;
013 src?: string;
014 href?: undefined;
015 target?: string;
016 })
017 | (HTMLAnchorAttributes & {
018 ref?: HTMLElement | null;
019 src?: string;
020 href: string;
021 target?: string;
022 });
023 </script>
024
025 <script lang="ts">
026 let {
027 class: className,
028 ref = $bindable(null),
029 src,
030 href,
031 target,
032 style,
033 children,
034 ...restProps
035 }: AvatarProps = $props();
036
037 const backgroundStyle = $derived(src ? `background-image: url(${src})` : '');
038 const tileStyle = $derived(
039 `${backgroundStyle}${backgroundStyle && style ? ';' : ''}${style ?? ''}`
040 );
041 const tileClass = $derived(`tile ${src ? '' : 'placeholder'} ${className ?? ''}`.trim());
042 </script>
043
044 {#if children}
045 <div class="parent" {...restProps as HTMLAttributes<HTMLDivElement>}>
046 {#if href}
047 <a bind:this={ref} class="tile" style={tileStyle} {href} {target} aria-label="avatar"></a>
048 {:else}
049 <figure bind:this={ref} class="tile" style={tileStyle}></figure>
050 {/if}
051 <span class="right">
052 {@render children?.()}
053 </span>
054 </div>
055 {:else if href}
056 <a
057 bind:this={ref}
058 class={tileClass}
059 style={tileStyle}
060 {href}
061 {target}
062 aria-label="avatar"
063 {...restProps as HTMLAnchorAttributes}
064 ></a>
065 {:else}
066 <figure
067 bind:this={ref}
068 class={tileClass}
069 style={tileStyle}
070 {...restProps as HTMLAttributes<HTMLElement>}
071 ></figure>
072 {/if}
073
074 <style>
075 .parent {
076 display: flex;
077 align-items: flex-start;
078 justify-content: space-between;
079 }
080
081 .right {
082 min-width: 10%;
083 width: 100%;
084 }
085
086 .tile,
087 a.tile {
088 display: inline-block;
089 width: 4ch;
090 height: calc(var(--line) * 3);
091 vertical-align: bottom;
092 flex-shrink: 0;
093 position: relative;
094 background-size: cover;
095 background-position: 50% 50%;
096 background-repeat: no-repeat;
097 }
098
099 .tile.placeholder {
100 background: var(--border-muted);
101 }
102
103 .tile::before {
104 content: '';
105 position: absolute;
106 top: 0;
107 left: 0;
108 right: 0;
109 bottom: 0;
110 pointer-events: none;
111 opacity: 0;
112 background: var(--focus-ring);
113 transition: opacity 0.2s ease-in-out;
114 }
115
116 .tile:hover::before,
117 .tile:active::before,
118 .tile:focus-visible::before {
119 opacity: 0.5;
120 }
121
122 .tile:focus-visible {
123 outline: none;
124 }
125 </style>
126