jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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.

install — avatar

$npx jixoai-ui add avatar

https://ui.jixoai.com/r/avatar.json

bevel · rounded · squircle sm halves the initials name tooltip on by default CJK-safe initials size IS the §1 axis

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.

Avatar usage
<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.

Ada Lovelace AL
Ada Lovelace A
avatar-usage.svelte svelte
<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%
avatar-silhouettes-demo.svelte svelte
<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

JX AoI hover or focus — the full name rides the default tooltip

decorative: alt=""

beside a visible name — the fallback block goes aria-hidden, no label
avatar-fallback-demo.svelte svelte
<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=&quot;&quot;</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

PropertyTypeDefaultDescription
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 ambientThe 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 booleantrueThe 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

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity 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.

PropertyTypeDefaultDescription
size 'small' | 'medium' | 'large' | 'auto' | numberambient scopeCONSUMED — 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' | numberambient scopeSUPPLY-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 scopeSUPPLY-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' | numberambient scopeSUPPLY-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 | stringambient scopeSUPPLY-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 scopeLANDS, 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' | numberambient scopeSUPPLY-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' | numberambient scopeSUPPLY-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)

avatar-axes.svelte svelte
<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

KeyAction
—Not interactive — an image; the name tooltip also opens on focus

ARIA

AttributeValueDescription
altname (default)The avatar is content; pass alt="" for decorative avatars beside a visible name.
roleimgOn the initials fallback block (omitted when decorative).
aria-labelnameOn the fallback block, keeping the label identical to the img path.
aria-hiddentrueOn the fallback block when alt="" marks it decorative.
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h