registry:ui · General
image — the no-CLS native picture
The general-purpose picture: lazy, async-decoded, REQUIRED intrinsic width and height (layout never shifts), alt semantics, failure fallback with src-change recovery. Lightbox is a dialog recipe, not built in.
$npx jixoai-ui add image
https://ui.jixoai.com/r/image.json
overview
Overview
A native img with three jobs the platform lacks: the no-CLS contract, alt semantics, and a failure posture that recovers.
Image renders one native <img>: loading="lazy", decoding="async", and REQUIRED intrinsic width/ height — the attributes carry the ratio while the rendered box stays max-width: 100% / height: auto, so the layout reserves the
box and never shifts. alt is the meaning; alt="" opts into
decorative. No lightbox, zoom or galleries — the batch-2 ruling keeps those a dialog
composition recipe.
The component's one added job is the failure posture: an onerror swap to
a fallback frame (dashed hairline, muted ground, the image glyph) that keeps the
intrinsic dims — failure never shifts layout either — and recovers when src changes (avatar's proven law; the consumer's own onerror is composed,
never replaced). A fallback snippet replaces the default frame wholesale;
keep the intrinsic dims in your slot markup. The §11 carriers JOIN the dimension
literals on the broken panel — the merge law — so the axis surface survives failure.
A LEAF in the fleet (grep receipt both directions: only the blueprint scene mounts it, and the family imports nothing but the icon — "on avatar's proven laws" is a law kinship, not a composition). The eight axes resolve on the img root all no-own (census batch D): seven stamp-and-supply, and theme is the frozen pole — the fallback face rides stylex defineVars aliases that resolve at the :root scope, so a dark island re-substitutes nothing (measured below). Per-axis below; the shared grammar lives on the universal props page.
usage
Usage
width and height are REQUIRED — the no-CLS contract is not optional.
<script lang="ts">
import Image from '@ui/image.svelte';
</script>
<!-- width/height are REQUIRED — the no-CLS contract is not optional -->
<Image src="/icon.svg" alt="the jixoai mark" width={96} height={96} />
<!-- alt="" opts into decorative; a failed load swaps to the fallback frame -->
<Image src={maybeBroken} alt="" width={96} height={96} />image
Left: a real load. Right: a broken source exercising the fallback — the dashed frame plus glyph; a later src change re-arms the load.

<script lang="ts">
import Image from '@ui/image.svelte';
</script>
<!-- width/height are REQUIRED — the no-CLS contract is not optional -->
<Image src="/icon.svg" alt="the jixoai mark" width={96} height={96} />
<!-- alt="" opts into decorative; a failed load swaps to the fallback frame -->
<Image src={maybeBroken} alt="" width={96} height={96} />law
The no-CLS contract
The img element carries loading, decoding and alt semantics; the component's one job is the failure posture — an onerror swap to a fallback frame that recovers when src changes. Intrinsic width/height stay REQUIRED: reserve the box, never shift the layout.
<script lang="ts">
import Image from '@ui/image.svelte';
</script>
<!-- width/height are REQUIRED — the no-CLS contract is not optional -->
<Image src="/icon.svg" alt="the jixoai mark" width={96} height={96} />
<!-- alt="" opts into decorative; a failed load swaps to the fallback frame -->
<Image src={maybeBroken} alt="" width={96} height={96} /><script lang="ts">
import Image from '@ui/image.svelte';
</script>
<!-- composed fallback: keep the intrinsic dims in your slot markup —
failure never shifts layout either. A changed src re-arms the load. -->
<Image src={flakyUrl} alt="build output" width={640} height={360}>
{#snippet fallback()}
<span style="width: 640px; height: 360px; display: grid; place-items: center;">
retry later
</span>
{/snippet}
</Image>image · composed fallback


<script lang="ts">
import Image from '@ui/image.svelte';
</script>
<!-- composed fallback: keep the intrinsic dims in your slot markup —
failure never shifts layout either. A changed src re-arms the load. -->
<Image src={flakyUrl} alt="build output" width={640} height={360}>
{#snippet fallback()}
<span style="width: 640px; height: 360px; display: grid; place-items: center;">
retry later
</span>
{/snippet}
</Image>api
Props
Props extend the native img attributes (Omit<'alt' | 'width' | 'height' | 'color'> — the required trio re-declared, the native color cleared for the axis); the eight axis rows render into the shared section beneath. 9 content rows here; no EXTRA lane — no family prop shares an axis name.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| alt * | string | — | The picture's meaning; "" marks it decorative. |
| width * | number | string | — | REQUIRED intrinsic width — the no-CLS contract. Never sees the size axis (the batch A native rule: the family owns only the axis names it destructures). |
| height * | number | string | — | REQUIRED intrinsic height — the no-CLS contract. |
| src | string | — | Via the native spread; a CHANGED src re-arms the load after failure (avatar's proven law). |
| fallback | Snippet | default frame | Composed failure state — keep the intrinsic dims in your slot markup; the default frame carries them so failure never shifts layout either. |
| onerror | native handler | — | COMPOSED, never replaced: your handler fires first, then the failure posture engages. |
| class | string | '' | Rendered width/height classes when different from intrinsic. |
| style | string | '' | Inline style passthrough — composed AFTER the family's carrier stamp (the #4 seam law). |
| ...rest | HTMLImgAttributes | spread | Every other native img attribute passes through (srcset, sizes, crossorigin…); Omit clears alt/width/height (re-declared required) and color (the §1 native-collision rule). |
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 image
An all-no-own first-time contract (census batch D, W3-D5) on a media leaf: the img paints pixels and the fallback frame is a fixed face, so nothing the axes know how to move exists here. Seven axes stamp-and-supply — and the supply matters only inside the COMPOSED fallback slot, whose content inherits the re-based channels. Theme is the frozen pole, and image is its purest case: every panel voice is a stylex defineVars alias, resolved at the :root scope before the panel ever inherits it. The carriers stamp the img root and JOIN the dimension literals on the broken panel (the merge law), greppable in the raw SSR.
Reading the table: Property is the axis, Type is the real carrier or consumption it drives on THIS family, Default is the lane default — the named steps, number unit, and consumption are in each description.
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY-ONLY, MEASURED INERT ON THE PAINT — the §11 stamp lands on the img root (the computed font-size moves to exactly the stamped px) and NOTHING follows: the box is the width/height ATTRIBUTES' own (the no-CLS contract), the fallback glyph is a fixed 32px prop, and pixels have no em voice (the measured contrast to heading's em ladder). Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | SUPPLY-ONLY — stamps --jx-shape-effective and --jx-radius-factor-effective; no family css reads them (grep receipt: zero corner declarations in ui/image/). The img draws no corners; the fallback panel is square. Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-radius-effective; no family css reads it (grep receipt: zero readers), and a leaf has no composed children to feed the concentric anchor. Number unit: px. |
| density | 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings) | 'auto' | SUPPLY-ONLY ON SELF, SUPPLIED TO THE SLOT — data-density stamps the img root AND the broken panel, and the family reads none of the re-based channels (grep receipt: the panel's padding rides the kernel --space-24 step via the --jx-space-24 alias, not the density channels). What the breakpoint moves is the COMPOSED fallback slot's content, which inherits the re-based channels. The merge law keeps the stamp on the panel across failure. Number unit: coefficient. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY-ONLY — stamps --jx-color-effective; no family css reads it (grep receipt: zero readers). The panel's ink is the muted alias, not the hue carrier. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | THE FROZEN POLE, MEASURED (the emission-form class) — the broken panel's ground/ink/border/padding ride tokens['--jx-muted'] / ['--jx-muted-foreground'] / ['--jx-border'] / ['--jx-space-24']: stylex defineVars aliases, declared (and therefore resolved) at the stylex :root theme scope. The panel inherits the already-substituted light values, so a scoped .dark re-substitutes NOTHING — probe receipt: --muted flips on the panel, --jx-muted does not, and the two panels compute byte-identical faces. The img itself paints pixels — nothing thematic at all. light and system stamp nothing — tree inheritance. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-elevation-effective; no family css reads it (grep receipt: zero shadow declarations — the fallback frame is flat, no art-shadow recipe). Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-motion-effective; no family css reads it. Zero transitions: lazy loading and async decode are the platform's own, not the axis's. Number unit: coefficient. |
Deviations, cited: the adoption is the census batch D row (the long tail —
openspec/changes/explicit-props/research/migration-census.md). The §1 collision rule:
the interface is Omit<HTMLImgAttributes, 'alt' | 'width' | 'height' | 'color'> — the required trio re-declared on the family's terms, the native color cleared for
the hue lane, and every axis name is the family's own destructured lane. Meta note:
image carries no generated meta yet (the ambient-vocabulary carriers bijection doesn't
know the family) — this table is the hand-table form; the universal section renders
from the shared schema either way.
<!-- theme: the FROZEN pole — the broken panel's ground, ink,
border and padding ride stylex defineVars aliases (declared, and
therefore resolved, at the stylex :root scope), so a dark island
re-substitutes NOTHING: the panel is byte-identical -->
<Image src="/definitely-missing.png" alt="dark island" width={96} height={96} theme="dark" />
<!-- size: the stamp lands on the img root and nothing follows — the
box is the width/height attributes' own, the glyph is a fixed
prop; pixels have no em voice -->
<Image src="/icon.svg" alt="stamped" width={96} height={96} size={14} />image · the frozen pole and the inert stamp


The theme pair is the frozen pole, measured: both fallback panels compute the same ground, border and ink — the voices are defineVars aliases resolved at the stylex :root scope (--muted flips on the panel; --jx-muted does not — the same emission-form receipt as hero's lead, with no raw-var voice to flip). The size pair is the inertness probe: the stamped img's computed font-size is exactly 14px while the rendered box stays the attributes' 96×96 — pixels have no em voice. Disclosure: the demos' usage mirrors above are HAND-AUTHORED to match the stage markup (the same-source migration is the recorded follow-up) — the component source in each drawer is the registry's own.
<!-- theme: the FROZEN pole — the broken panel's ground, ink,
border and padding ride stylex defineVars aliases (declared, and
therefore resolved, at the stylex :root scope), so a dark island
re-substitutes NOTHING: the panel is byte-identical -->
<Image src="/definitely-missing.png" alt="dark island" width={96} height={96} theme="dark" />
<!-- size: the stamp lands on the img root and nothing follows — the
box is the width/height attributes' own, the glyph is a fixed
prop; pixels have no em voice -->
<Image src="/icon.svg" alt="stamped" width={96} height={96} size={14} /><script lang="ts">
import Image from '@ui/image.svelte';
import { query } from '@lib/universal-props-query.svelte';
import type { DensityLane } from '@lib/defaults.svelte';
</script>
<!-- below 48rem the base (large rung) applies; at 48rem+ the md case
wins and the DEFAULT fallback frame stamps sm — the data-density
on the broken panel flips with the viewport. No fallback snippet
here on purpose: the snippet branch renders YOUR markup only
(nothing stamps it). -->
<Image
src="/definitely-missing.png"
alt="responsive failure"
width={640}
height={360}
density={query<{ md: DensityLane }, DensityLane>({ md: 'small' }, 'large')}
/>image · query()
Media keys are min-width: below 48rem the base applies — the large rung; at 48rem and wider the md case wins and the DEFAULT fallback frame stamps sm (the broken panel's data-density flips lg ↔ sm across the breakpoint — the family's own panel, measured). No fallback snippet here ON PURPOSE: the snippet branch renders your markup only — nothing stamps it — so the default frame is the honest demo surface. The string lane takes both generics.
<script lang="ts">
import Image from '@ui/image.svelte';
import { query } from '@lib/universal-props-query.svelte';
import type { DensityLane } from '@lib/defaults.svelte';
</script>
<!-- below 48rem the base (large rung) applies; at 48rem+ the md case
wins and the DEFAULT fallback frame stamps sm — the data-density
on the broken panel flips with the viewport. No fallback snippet
here on purpose: the snippet branch renders YOUR markup only
(nothing stamps it). -->
<Image
src="/definitely-missing.png"
alt="responsive failure"
width={640}
height={360}
density={query<{ md: DensityLane }, DensityLane>({ md: 'small' }, 'large')}
/>| Token | Default |
|---|---|
| the intrinsic box | width/height — REQUIRED attributes |
| the merge law | carriers + width/height literals |
| the fallback face | dashed hairline · muted ground · muted-foreground ink |
| the panel padding | --jx-space-24 (24px) |
| the fallback glyph | 32px · strokeWidth 1.5 |
| the hooks | data-jx-image · data-jx-image-broken |
a11y
Accessibility
A native img with real alt semantics; the fallback keeps the name and stays decorative when alt is empty.
Keyboard
| Key | Action |
|---|---|
| — | Not interactive — a picture with alt semantics |
ARIA
| Attribute | Value | Description |
|---|---|---|
| alt | string (required) | The picture's meaning; "" marks it decorative. |
| role / aria-label | img / "image unavailable" | On the default fallback frame — only when alt is non-empty. |
| aria-hidden | true | On the fallback when alt="" keeps the picture decorative through failure. |