registry:ui · NativeHTML
avatar — an img, honestly
The avatar IS an <img>: lazy, async-decoded, intrinsic width/height so layout never shifts. When the source fails or is absent, it swaps to an initials block derived code-point-wise from the name — CJK-safe (张伟 stays 张伟), halved to one code point at icon size so the badge never wraps. Three silhouettes ride one geometry: the bevel radius law (default), a true circle, and the squircle superellipse. Hover any avatar and the full name comes back on a tooltip — on by default, tooltip=false opts out.
$npx jixoai-ui add avatar
https://ui.jixoai.com/r/avatar.json
overview
Overview
An img with a failure posture: the initials fallback, the tooltip that gives the name back, and a size prop that is the universal size axis outright.
The img element carries loading, decoding, intrinsic sizing, and alt semantics. The one
thing it lacks is a failure posture — that is the whole component: an onerror swap to an
initials block derived code-point-wise from name, and nothing else. One
word → its first two code points (张伟 stays 张伟 — the slicing is code-point-wise);
several words → the first letters of the first and last; the 24px box keeps only the
first.
The silhouette is the family's own variant literal — bevel (the radius
law), rounded (the circle), squircle (the superellipse) — no collision with the shape
axis, which is a separate, supply-side lane here. The size prop is the universal size
axis outright (the §13 adoption): named steps 24 / 32 / 40, the legacy sm/md/lg
spellings alias onto them, and a number is the box edge in px verbatim. The full name
rides a tooltip by default — an avatar crops identity to initials, the tooltip gives it
back; pass alt="" for a decorative avatar beside a visible name. The axis
grammar lives on the universal props page.
usage
Usage
Give it a name; the image is optional — the fallback covers failed or missing sources.
<script lang="ts">
import Avatar from '@ui/avatar.svelte';
</script>
<Avatar src="/team/ada.png" name="Ada Lovelace" />
<Avatar name="张伟" /> <!-- initials fallback: 张伟 -->
<Avatar name="Gaubee" size="lg" />avatar
The left avatar loads a real image; the right one has no source and shows the initials fallback derived live from the playground's name field. Every instance re-corners with the silhouette pick — and at sm the fallback halves to one code point. Hover (or focus) an avatar: the full name rides the default tooltip.
<script lang="ts">
import Avatar from '@ui/avatar.svelte';
</script>
<Avatar src="/icon.svg" name="Ada Lovelace" variant="bevel" size="lg" />
<Avatar name="Ada Lovelace" variant="bevel" size="lg" />corner-shape law
One geometry, three corners
The silhouette is one CSS decision layered on the same box: bevel keeps the jixoai radius law with var(--radius) riding the md baseline and scaled by the same proportion at sm and lg (6 / 8 / 10px — square where corner-shape is unsupported, the §14 factor composing to 0), rounded states corner-shape: round with a 50% radius for a true circle, and squircle states corner-shape: squircle with the same 50% for the superellipse — engines without corner-shape simply round it back to the circle. Nothing degrades ugly.
avatar · silhouettes
corner-shape: bevel + var(--radius) × 0.75 / 1 / 1.25 (6·8·10px)corner-shape: round + border-radius: 50%corner-shape: squircle + border-radius: 50%<script lang="ts">
import Avatar from '@ui/avatar.svelte';
</script>
<div class={cx(rt.col16)}>
<div class={cx(rt.wrapRow16)}>
<Avatar name="张伟" variant="bevel" size="lg" alt="" />
<Avatar name="JX AoI" variant="bevel" size="md" alt="" />
<Avatar name="JX AoI" variant="bevel" size="sm" alt="" />
<code class={cx(rt.inkAccent, rt.text115, rt.lead5)}>corner-shape: bevel + var(--radius) × 0.75 / 1 / 1.25 (6·8·10px)</code>
</div>
<div class={cx(rt.wrapRow16)}>
<Avatar name="张伟" variant="rounded" size="lg" alt="" />
<Avatar name="JX AoI" variant="rounded" size="md" alt="" />
<Avatar name="JX AoI" variant="rounded" size="sm" alt="" />
<code class={cx(rt.inkAccent, rt.text115, rt.lead5)}>corner-shape: round + border-radius: 50%</code>
</div>
<div class={cx(rt.wrapRow16)}>
<Avatar name="张伟" variant="squircle" size="lg" alt="" />
<Avatar name="JX AoI" variant="squircle" size="md" alt="" />
<Avatar name="JX AoI" variant="squircle" size="sm" alt="" />
<code class={cx(rt.inkAccent, rt.text115, rt.lead5)}>corner-shape: squircle + border-radius: 50%</code>
</div>
</div>the failure posture
The initials fallback and the tooltip
No source, or a failed one: the img swaps to an initials block — the algorithm is deterministic, code-point-wise, and halves at icon size. The full name rides the default tooltip; alt decides whether the avatar speaks.
avatar · fallback and tooltip
the initials algorithm
AL · GA · 张伟 — first+last initials, two code points of one word, CJK-safe
sm halves the block
one code point — two full-width glyphs cannot fit 24px, and a badge must never wrap
image + tooltip
decorative: alt=""
<script lang="ts">
import Avatar from '@ui/avatar.svelte';
</script>
<div class={cx(rt.gridSm2)}>
<div class={cx(rt.panel)}>
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>the initials algorithm</p>
<div class={cx(rt.rowC12)}>
<Avatar name="Ada Lovelace" alt="" />
<Avatar name="Gaubee" alt="" />
<Avatar name="张伟" alt="" />
</div>
<p class={cx(rt.mt8, rt.text12, rt.inkMuted)}>AL · GA · 张伟 — first+last initials, two code points of one word, CJK-safe</p>
</div>
<div class={cx(rt.panel)}>
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>sm halves the block</p>
<div class={cx(rt.rowC12)}>
<Avatar name="Ada Lovelace" size="sm" alt="" />
<Avatar name="张伟" size="sm" alt="" />
</div>
<p class={cx(rt.mt8, rt.text12, rt.inkMuted)}>one code point — two full-width glyphs cannot fit 24px, and a badge must never wrap</p>
</div>
<div class={cx(rt.panel)}>
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>image + tooltip</p>
<div class={cx(rt.rowC12)}>
<Avatar src="/icon.svg" name="JX AoI" size="lg" />
<span class={cx(rt.noteSmall)}>hover or focus — the full name rides the default tooltip</span>
</div>
</div>
<div class={cx(rt.panel)}>
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>decorative: alt=""</p>
<div class={cx(rt.rowC12)}>
<Avatar name="JX AoI" alt="" />
<span class={cx(rt.noteSmall)}>beside a visible name — the fallback block goes aria-hidden, no label</span>
</div>
</div>
</div>api
API
The table renders from the GENERATED meta; the eight axis props split into the shared Universal props section beneath the family rows (size among them — the §13 adoption means the family prop IS the axis; its story is told in the axes table below).
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| src | string | — | Image URL. A failed load (or no src) swaps to the initials block — the onerror swap is the whole component; a changed src is a fresh chance (the failure state resets). |
| name * | string | — | The person — fuels alt text, the initials fallback and the tooltip. One word → its first two code points (CJK-safe: 张伟 stays 张伟); several words → the first letters of the first and last; the sm box keeps only the first code point. |
| alt | string | — | Defaults to `name` (the avatar is content). Pass "" explicitly for a decorative avatar beside a visible name — the fallback block honors it too (aria-hidden, no label). |
| variant | 'bevel | rounded | squircle' | 'bevel' · Own default, not ambient · Own default, not ambient | The silhouette — one geometry, three corners, the family's own literal slot: bevel cuts var(--radius) to the box (sm 0.75× · md 1× · lg 1.25× — 6 / 8 / 10px at the 8px baseline), rounded is the true circle (50%), squircle the superellipse (50%). Not the §2 shape axis — no name collision; the axis supplies downward (see the axes table). corner-shape degrades per the §14 ladder: bevel → square, rounded/squircle → circle. |
| tooltip | boolean | true | The full name rides a tooltip (hover-intent + focus, the tooltip family laws) — an avatar crops identity to initials, the tooltip gives it back. Pass false when the name is already visible beside the avatar. |
| class | string | '' | Appended to the composed classes — forwarded to the img / fallback block. |
| onerror | unknown | — | Runs BEFORE the fallback swap (a throwing handler must not leave the broken img on screen). Inspect event.target only — the swap already happened by the time your handler returns. |
| style | unknown | — | Inline style — joined AFTER the axis carriers and the box var (the merge law: caller wins). |
| rest | unknown (spread passthrough) | — | Every other native img attribute flows through onto the img root. loading="lazy", decoding="async" and the intrinsic width/height are the family's own (layout never shifts); caller values override. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
axes
The eight axes on this component
What each axis drives HERE — the carrier names are the family's real vars (avatar.stylex.ts, avatar.css, stampCarriersForLanes), steps and units per universal-props.schema.ts, every axis defaulting auto. Size is the headline: the family prop IS the axis (the §13 adoption — the fleet's first). Theme lands the .dark bridge and paints nothing in the built pipeline (a documented absence, the W-next #1 gap). The other six are supply-only: the avatar is a leaf, its css reads none of their carriers, and each row carries the negative-grep receipt.
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | ambient scope | CONSUMED — the §13 adoption: this prop IS the axis. Named steps keep the box ladder (small 24 · medium 32 · large 40 — measured), the legacy sm/md/lg spellings alias onto them pre-resolve (AVATAR_SIZE_ALIASES), and a number IS the box edge in px verbatim — the number lane stamps the box var (--jx-avatar-md) the md atom reads, so 48 is a 48px avatar. auto inherits the ambient font-size context (the 32px geometry baseline; a list-item media host may inject its own square through the same seam). The initials voice FOLLOWS the size: at ambient (no explicit lane) the fallback block's fixed control-label step applies (--jx-text-label-lg, 12px — the one true fixed case), but an explicit lane stamps the §11 echo (font-size: var(--jx-size-effective, 1rem)) INLINE on the root, and inline beats the class token — measured 14 / 16 / 18px at small / medium / large, and the box edge verbatim on the number lane (48px initials at size={48}). At 48 the two-letter block overflows its own box (scrollWidth 52 > clientWidth 46 — visibly clipped; 28 clips mildly, 30/26): the sm box's halve-to-one-code-point rule keeps the 24px badge safe, but a large number lane has no such guard — the family-level clip is drift ledger #7 (fixed-paint follow-up; do not scale the block in app code — pass tooltip and a larger box, or halve the name yourself). |
| density | 'small' | 'medium' | 'large' | 'xs' | '2xs' | 'sm' | 'default' | 'lg' | 'auto' | number | ambient scope | SUPPLY-ONLY — the rung stamps data-density on the img/fallback root, and nothing reads it: the avatar reads no density channel (the initials voice is the fixed --jx-text-label-lg step AT AMBIENT ONLY — an explicit size lane replaces it through the §11 echo, see the size row; there is no padding, gap or line to re-base — grep receipt: zero channel reads in the family css). The tooltip shell is the body's DOM ANCESTOR, so the rung's scope never re-scopes it either. A coefficient stamps --jx-density-coefficient — no scope block matches the leaf, nothing recomposes. Measured: box and initials unmoved under a stamped lg rung. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | ambient scope | SUPPLY-ONLY — stamps --jx-shape-effective for the concentric chain; the silhouette sheet reads the §14 ALIAS ladder instead (corner-shape: var(--jx-shape-bevel/round/squircle), keyed on the variant), never the carrier (grep receipt: zero --jx-shape-effective reads). The corners are the variant's law (see the silhouettes above), not the axis's. Measured: a stamped carrier leaves the corner. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | ambient scope | SUPPLY-ONLY — stamps --jx-radius-effective; the bevel cut composes var(--radius) × the per-shape factor (--jx-radius-factor-bevel: 1 supported / 0 degraded), the site radius token — not the axis carrier (grep receipt: zero --jx-radius-effective reads). Measured: a stamped 24px carrier under the 8px md cut leaves the corner exactly. Documented absence on the avatar itself; the leaf has no nested consumers to broadcast to. Number unit: px. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | ambient scope | SUPPLY-ONLY — stamps --jx-color-effective (named → the §12 var indirection; a number → hue degrees; raw strings pass through, closed at build). The initials ink reads the theme token --jx-muted-foreground directly and nothing in the family css reads the carrier (grep receipt: zero reads). Measured: a stamped cyan carrier leaves the ink. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | ambient scope | LANDS, PAINTS NOTHING — a documented absence. A resolved dark step puts the .dark class bridge on the img/fallback root, and the family's four voices are theme-ALIASED stylex tokens (--jx-border, --jx-card, --jx-muted, --jx-muted-foreground) whose emission resolves at :root — a plain .dark island re-scopes none of them in the built pipeline (the semantic-ink re-scope gap, the drift ledger's W-next #1). Measured: the dark island leaves frame, ground and initials ink at their light values. light and system stamp nothing: they ride tree inheritance. No number lane. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | ambient scope | SUPPLY-ONLY — stamps --jx-elevation-effective; the avatar's one edge is the 1px hairline (--jx-hairline) and there is no shadow to step (grep receipt: zero reads, and no shadow property in the family css). Measured: box-shadow unchanged (none) under a stamped level. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | ambient scope | SUPPLY-ONLY — stamps --jx-motion-effective; the family declares no transition (the name tooltip's motion belongs to the tooltip family, not the avatar — grep receipt: zero --jx-motion-effective reads and no transition in avatar.css/avatar.stylex.ts). A number is a coefficient: nothing on this leaf steps. Number unit: coefficient. |
Deviations, cited: the six supply-only lanes (density · shape · radius · color ·
elevation · motion) — the family consumes none of their carriers, per the broadcast
protocol (吃也供, supply-and-consume; the universal-props concept page owns the term);
their CARRIERS have no descendant reader in the tree, and the leaf stamps them for
consumer compositions alone. Size is the exception and reads in TWO places: the §13
adoption consumes the lane through the family prop (the box vars the atoms read), and
the §11 echo stamps the root's own font-size inline — the carrier var itself
(--jx-size-effective) has no descendant css reader. The §13 size
adoption and the batch-B axis surface are recorded in migration-census.md (openspec/changes/explicit-props/research, the W3 rows). The theme axis lands its
bridge and paints nothing — the W-next #1 gap, named per voice in the row above. The
number-lane initials clip is drift ledger #7 (family-level, the fixed-paint follow-up).
The size axis, live
The named steps keep the 24/32/40 ladder, the legacy aliases ride the same resolve, the number lane is the box edge in px verbatim (48 and 28), and one real query() case — 40px below the lg viewport rung, 48px at ≥64rem (resize the window). The dark island lands the bridge and paints nothing: every voice stays at its light value — the documented absence.
named steps · 24 / 32 / 40px (measured)
legacy aliases · sm/md/lg → small/medium/large, the same boxes
number lane · the box edge in px verbatim (48 / 28) — the initials follow the edge too (the §11 inline echo beats the fixed label step), and 48 clips its two letters (scrollWidth 52 > clientWidth 46 — drift ledger #7)
query() · 40px below the lg rung, 48px at ≥64rem — resize the window
theme dark · the bridge lands, nothing repaints — the documented absence (W-next #1)
<script lang="ts">
import Avatar from '@ui/avatar.svelte';
import { query } from '@lib/universal-props-query.svelte';
import type { SizeLane } from '@lib/defaults.svelte';
</script>
<div class={cx(rt.gridSm2)}>
<div class={cx(rt.panel)}>
<div class={cx(rt.rowC12)}>
<Avatar name="Ada Lovelace" size="small" alt="" />
<Avatar name="Ada Lovelace" size="medium" alt="" />
<Avatar name="Ada Lovelace" size="large" alt="" />
</div>
<p class={cx(rt.mt8, rt.text12, rt.inkMuted)}>named steps · 24 / 32 / 40px (measured)</p>
</div>
<div class={cx(rt.panel)}>
<div class={cx(rt.rowC12)}>
<Avatar name="Ada Lovelace" size="sm" alt="" />
<Avatar name="Ada Lovelace" size="md" alt="" />
<Avatar name="Ada Lovelace" size="lg" alt="" />
</div>
<p class={cx(rt.mt8, rt.text12, rt.inkMuted)}>legacy aliases · sm/md/lg → small/medium/large, the same boxes</p>
</div>
<div class={cx(rt.panel)}>
<div class={cx(rt.rowC12)}>
<Avatar name="Ada Lovelace" size={48} alt="" />
<Avatar name="Ada Lovelace" size={28} alt="" />
</div>
<p class={cx(rt.mt8, rt.text12, rt.inkMuted)}>number lane · the box edge in px verbatim (48 / 28) — the initials follow the edge too (the §11 inline echo beats the fixed label step), and 48 clips its two letters (scrollWidth 52 > clientWidth 46 — drift ledger #7)</p>
</div>
<div class={cx(rt.panel)}>
<Avatar name="Ada Lovelace" size={query<{ lg: number }, number>({ lg: 48 }, 40)} alt="" />
<p class={cx(rt.mt8, rt.text12, rt.inkMuted)}>query() · 40px below the lg rung, 48px at ≥64rem — resize the window</p>
</div>
<div class={cx(rt.panel)}>
<div class={cx(rt.rowC12)}>
<Avatar name="Ada Lovelace" alt="" />
<Avatar name="Ada Lovelace" theme="dark" alt="" />
</div>
<p class={cx(rt.mt8, rt.text12, rt.inkMuted)}>theme dark · the bridge lands, nothing repaints — the documented absence (W-next #1)</p>
</div>
</div>a11y
Accessibility
The avatar is content: alt defaults to the name, and the fallback block keeps the same label with role=img. Not interactive — the name tooltip also opens on focus.
Keyboard
| Key | Action |
|---|---|
| — | Not interactive — an image; the name tooltip also opens on focus |
ARIA
| Attribute | Value | Description |
|---|---|---|
| alt | name (default) | The avatar is content; pass alt="" for decorative avatars beside a visible name. |
| role | img | On the initials fallback block (omitted when decorative). |
| aria-label | name | On the fallback block, keeping the label identical to the img path. |
| aria-hidden | true | On the fallback block when alt="" marks it decorative. |