@ -6,12 +6,18 @@
type SkeletonColor
} from '$uix/eidos/components/skeleton';
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { compileMorfo } from '$uix/morfo';
import { skeletonMorfo } from '@/uix/morfo/components/skeleton';
import { getActiveUix } from '$active-uix';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event : string ; family : string ; intent? : string ; at : number } ;
const uix = getActiveUix();
let tab = $state< Tab > ('live');
let trace = $state< TraceEntry [ ] > ([]);
let stageRef = $state< HTMLElement | null > (null);
let shape = $state< SkeletonShape > ('rect');
let size = $state< SkeletonSize > ('md');
@ -38,6 +44,51 @@
'loss'
];
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes : true , subtree : true , attributeFilter : [ 'data-event' ] } );
return () => obs.disconnect();
});
const somaSnippet = $derived(
[
'<!-- Skeleton is eidos - native — no soma layer. --> ',
'<!-- Equivalent semantic markup (not real soma): --> ',
'',
'< div ' ,
' data-skeleton',
shape !== 'rect' && ` data-shape="${ shape } "`,
size !== 'md' && ` data-size="${ size } "`,
color !== 'neutral' && ` data-color="${ color } "`,
animated & & ' data-animated=""',
' role="status"',
' aria-busy="true"',
' aria-label="Loading"',
shape === 'rect' && ` style="inline-size: ${ width } ; block-size: ${ height } ;"`,
'>< / div > '
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
@ -65,21 +116,36 @@
< h1 data-uix-page-title > Skeleton< / h1 >
< p data-uix-page-lede >
Content placeholder shown while data loads. Three shapes
(< code > rect< / code > , < code > circle< / code > , < code > text< / code > ), five sizes, eight color accents,
and an optional shimmer animation. Pure eidos primitive — no semantic events, no soma layer.
Reduced-motion freezes the animation while keeping the visual.
(< code > rect< / code > , < code > circle< / code > , < code > text< / code > ), five sizes, eight color
accents, and an optional shimmer animation. Eidos-native: no semantic events, no soma
split — the morfo declares one Provider part with < code > role="status"< / code > and
< code > aria-busy="true"< / code > .
< / p >
< div data-uix-page-meta >
< span data-uix-meta-pill >< span data-uix-meta-key > parts</ span > { partsList . length } </ span >
< span data-uix-meta-pill > < span data-uix-meta-key > events< / span > 0< / span >
< span data-uix-meta-pill >< span data-uix-meta-key > shapes</ span > { shapes . length } </ span >
< span data-uix-meta-pill > < span data-uix-meta-key > scope< / span > eidos< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > parts</ span > { compiled . parts . order . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > events< / span > 0
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > shapes</ span > { shapes . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > sizes</ span > { sizes . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > scope< / span > eidos
< / span >
< / div >
< / header >
<!-- Live preview always rendered (between header and tabs). -->
< div data-uix-stage >
< div data-uix-stage-area >
< Box style = "padding: var(--uix-space-4); display: flex; justify-content: center;" >
< div data-uix-stage-area bind:this = { stageRef } >
< Box
style="padding: var(--uix-space-4); display: flex; justify-content: center; inline-size: 100%;"
>
< Box style = "inline-size: min(28rem, 100%);" >
{ #if shape === 'rect' }
< Skeleton { shape } { size } { color } { animated } { width } { height } />
@ -91,20 +157,31 @@
< / Box >
< / Box >
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
< span > { trace . length === 0 ? 'no semantic events' : `$ { trace . length } event ( s ) ` } </ span >
< span style = "color: var(--uix-text-faint)" > ·< / span >
< span data-uix-stage-trace-key > shape< / span >
< span > { shape } </ span >
< span style = "margin-inline-start: auto;" >
< span data-uix-stage-trace-key > size< / span >
{ size } · < span data-uix-stage-trace-key > color</ span > { color }
< / span >
< / div >
< / div >
< div data-uix-tabs role = "tablist" >
< button data-uix-tab data-active = { tab === 'live' } onclick= {() => ( tab = 'live' )} > Live</ button >
< button data-uix-tab data-active = { tab === 'api' } onclick= {() => ( tab = 'api' )}
>API < span data-uix-tab-count > 7< / span > < /button
>
< button data-uix-tab data-active = { tab === 'api' } onclick= {() => ( tab = 'api' )} >
API < span data-uix-tab-count > 7< / span >
< / button >
< button data-uix-tab data-active = { tab === 'morfo' } onclick= {() => ( tab = 'morfo' )} >
< span data-uix-layer-badge = "morfo" > morfo< / span >
< span data-uix-tab-count > { partsList . length } p · 0e</ span >
< / button >
< button data-uix-tab data-active = { tab === 'sema' } onclick= {() => ( tab = 'sema' )} >
< span data-uix-layer-badge = "sema" > sema< / span >
< span data-uix-tab-count > 0< /span >
< span data-uix-tab-count >{ events . length } < /span >
< / button >
< button data-uix-tab data-active = { tab === 'recipe' } onclick= {() => ( tab = 'recipe' )} > Recipe< /button
>
@ -114,9 +191,14 @@
{ #if tab === 'live' }
< section data-uix-section >
< h2 data-uix-section-title > Controls< / h2 >
< p data-uix-section-desc >
Skeleton is eidos-native — no < span data-uix-layer-badge = "soma" > soma< / span > split. The
< code > shape< / code > prop drives the recipe geometry; < code > size< / code > sets intrinsic
block-size; < code > color< / code > tints the placeholder background.
< / p >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > props · shape and bounds
< span data-uix-layer-badge = "eidos" > eidos< / span > props · visual treatment
< / div >
< div data-uix-controls >
< label data-uix-control >
@ -170,119 +252,370 @@
{ /if }
< / div >
< div data-uix-subsection-head > Composition examples< / div >
< Stack gap = "var(--uix-space-4)" >
< div data-uix-section-desc > Avatar + 2 lines< / div >
< Box
style="display: flex; gap: var(--uix-space-3); align-items: center; padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
>
< Skeleton shape = "circle" size = "lg" / >
< Box style = "flex: 1;" >
< Skeleton shape = "text" lines = { 2 } / >
< / Box >
< / Box >
<!-- ── Code snippets per layer ─────────────────────────────────── -->
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "soma" > soma< / span >
< span > n/a · skeleton is eidos-native — equivalent markup shown< / span >
< span data-uix-code-lang > svelte< / span >
< / div >
< pre >< code > { somaSnippet } </ code ></ pre >
< / div >
< div data-uix-section-desc > Card with image + content< / div >
< Box
style="border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); overflow: hidden;"
>
< Skeleton width = "100%" height = "160px" / >
< Box style = "padding: var(--uix-space-4);" >
< Skeleton shape = "text" lines = { 3 } / >
< / Box >
< / Box >
< / Stack >
< / section >
{ :else if tab === 'api' }
< section data-uix-section >
< h2 data-uix-section-title > API< / h2 >
< table data-uix-api-table >
< thead >
< tr > < th > prop< / th > < th > type< / th > < th > default< / th > < th > description< / th > < / tr >
< / thead >
< tbody >
< tr > < td > shape< / td > < td > 'rect' | 'circle' | 'text'< / td > < td > 'rect'< / td > < td > Visual shape.< / td > < / tr >
< tr > < td > size< / td > < td > 'xs'..'xl'< / td > < td > 'md'< / td > < td > Intrinsic block-size.< / td > < / tr >
< tr > < td > color< / td > < td > ColorRole< / td > < td > 'neutral'< / td > < td > Placeholder tint.< / td > < / tr >
< tr > < td > lines< / td > < td > number< / td > < td > 1< / td > < td > Bar count when shape='text'.< / td > < / tr >
< tr > < td > animated< / td > < td > boolean< / td > < td > true< / td > < td > Shimmer wave (CSS keyframes).< / td > < / tr >
< tr > < td > aria-label< / td > < td > string?< / td > < td > —< / td > < td > Override the translated 'Loading'.< / td > < / tr >
< tr >
< td > ...Box< / td > < td > BoxProps< / td > < td > —< / td > < td > width/height/padding/gridColumn/…< / td >
< / tr >
< / tbody >
< / table >
< div data-uix-code style = "margin-top: var(--uix-space-3);" >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > eidos< / span >
< span > visual · shape + size drive recipe geometry< / span >
< span data-uix-code-lang > svelte< / span >
< / div >
< pre >< code > { eidosSnippet } </ code ></ pre >
< / div >
< / section >
{ :else if tab === 'morfo' }
{ /if }
{ #if tab === 'api' }
< section data-uix-section >
< h2 data-uix-section-title >
< span data-uix-layer-badge = "morfo" > morfo< / span > contract
< / h2 >
< h2 data-uix-section-title > API reference< / h2 >
< p data-uix-section-desc >
One provider part with < code > role="status"< / code > and < code > aria-busy="true"< / code > . Carries
< code > data-shape< / code > + < code > data-animated< / code > attrs that the eidos recipe consumes .
Skeleton accepts shape / size / color / lines / animated and inherits the full Box prop
surface (width, height, padding, gridColumn, …) so it composes inside any layout.
< / p >
< table data-uix-api-table >
< thead > < tr > < th > part< / th > < th > element< / th > < th > role< / th > < th > archetype< / th > < / tr > < / thead >
< tbody >
{ #each partsList as p ( p . kebab )}
< tr >
< td >< code > { p . kebab } </ code ></ td >
< td >< code > { p . defaultElement } </ code ></ td >
< td > { p . role ?? '—' } </ td >
< td > { p . archetype ?? '—' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< div data-uix-subsection-head > Skeleton-specific props< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Prop< / th > < th > Type< / th > < th > Notes< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > shape< / td >
< td class = "type" > 'rect' | 'circle' | 'text'< / td >
< td >
< code > rect< / code > (default) → rounded rectangle. < code > circle< / code > → 1:1 ratio,
fully rounded. < code > text< / code > → stacked thin bars at staggered widths.
< / td >
< / tr >
< tr >
< td class = "name" > size< / td >
< td class = "type" > 'xs' | 'sm' | 'md' | 'lg' | 'xl'< / td >
< td > Intrinsic block-size when no < code > height< / code > is set. Default < code > md< / code > .< / td >
< / tr >
< tr >
< td class = "name" > color< / td >
< td class = "type" > ColorRole< / td >
< td >
Placeholder tint. < code > neutral< / code > (default) for content-agnostic
placeholders; intent colors for failure states.
< / td >
< / tr >
< tr >
< td class = "name" > lines< / td >
< td class = "type" > number< / td >
< td >
Bar count when < code > shape='text'< / code > . Each bar's width staggers so the block
reads as a paragraph. Default < code > 1< / code > .
< / td >
< / tr >
< tr >
< td class = "name" > animated< / td >
< td class = "type" > boolean< / td >
< td >
Shimmer wave via CSS background-position keyframes. Frozen under
< code > prefers-reduced-motion: reduce< / code > . Default < code > true< / code > .
< / td >
< / tr >
< tr >
< td class = "name" > aria-label< / td >
< td class = "type" > string?< / td >
< td > Override the translated 'Loading' default — useful for context-specific messages.< / td >
< / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Inherited from Box< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Prop< / th > < th > Type< / th > < th > Notes< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > width / height< / td >
< td class = "type" > number | string< / td >
< td > Override intrinsic size — pass any CSS length.< / td >
< / tr >
< tr >
< td class = "name" > padding / margin (and per-side)< / td >
< td class = "type" > number | string< / td >
< td > Numbers map to < code > var(--space-N)< / code > .< / td >
< / tr >
< tr >
< td class = "name" > gridColumn / gridRow / placeSelf< / td >
< td class = "type" > string< / td >
< td > Use Skeleton as a grid item — placement props live on Box.< / td >
< / tr >
< tr >
< td class = "name" > …< / td >
< td class = "type" > BoxProps< / td >
< td > See < a href = "/uix/components/box" > < Box> < / a > for the full surface.< / td >
< / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Reference comparison< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Library< / th > < th > Closest equivalent< / th > < th > Difference< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > shadcn/ui< / td >
< td > < code > < Skeleton> < / code > < / td >
< td > shadcn ships a single rectangular class. UIX adds shape variants + the text-paragraph composition out of the box.< / td >
< / tr >
< tr >
< td class = "name" > chakra-ui< / td >
< td > < code > < Skeleton> < / code > + < code > < SkeletonText> < / code > < / td >
< td > Chakra splits into two components. UIX folds both into one with < code > shape='text'< / code > .< / td >
< / tr >
< tr >
< td class = "name" > mantine< / td >
< td > < code > < Skeleton> < / code > + < code > < SkeletonText> < / code > < / td >
< td > Same split as Chakra; UIX collapses to one component with < code > lines< / code > for paragraphs.< / td >
< / tr >
< tr >
< td class = "name" > radix-themes< / td >
< td > < code > < Skeleton> < / code > < / td >
< td > Radix uses an inert `data-skeleton` over arbitrary children. UIX exposes structured shape variants.< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'morfo' }
< section data-uix-section >
< h2 data-uix-section-title > Morfo contract< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Field< / th > < th > Value< / th > < / tr > < / thead >
< tbody >
< tr >< td class = "name" > name</ td >< td > { skeletonMorfo . name } </ td ></ tr >
< tr >< td class = "name" > kebab</ td >< td >< code > { skeletonMorfo . kebab } </ code ></ td ></ tr >
< tr >< td class = "name" > scope</ td >< td > { skeletonMorfo . scope . join ( ', ' )} </ td ></ tr >
< tr >< td class = "name" > parts</ td >< td > { partsList . length } </ td ></ tr >
< tr > < td class = "name" > events< / td > < td > 0< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Parts< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead >
< tr >
< th > kebab< / th >
< th > marker< / th >
< th > element< / th >
< th > archetype< / th >
< th > role< / th >
< th > optional< / th >
< / tr >
< / thead >
< tbody >
{ #each partsList as part ( part . kebab )}
< tr >
< td class = "name" > { part . kebab } </ td >
< td >< code data-uix-part-marker > [{ part . marker } ]</ code ></ td >
< td class = "type" > < { part . defaultElement } > </ td >
< td class = "type" > { part . archetype ?? '—' } </ td >
< td class = "type" > { part . role ?? '—' } </ td >
< td class = "default" > { part . optional ? 'yes' : 'no' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Provider · data attrs< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > attr< / th > < th > values< / th > < th > source< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > < code > data-shape< / code > < / td >
< td > rect | circle | text< / td >
< td class = "type" > propRef('shape')< / td >
< / tr >
< tr >
< td class = "name" > < code > data-animated< / code > < / td >
< td > '' (when on)< / td >
< td class = "type" > propRef('animated')< / td >
< / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Provider · aria< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > attr< / th > < th > value< / th > < th > severity< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > < code > role< / code > < / td >
< td > status< / td >
< td class = "type" > part default< / td >
< / tr >
< tr >
< td class = "name" > < code > aria-label< / code > < / td >
< td > translationRef('components.skeleton.label')< / td >
< td class = "type" > recommended< / td >
< / tr >
< tr >
< td class = "name" > < code > aria-busy< / code > < / td >
< td > "true"< / td >
< td class = "type" > literal< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
{ :else if tab === 'sema' }
{ /if }
{ #if tab === 'sema' }
< section data-uix-section >
< h2 data-uix-section-title >
< span data-uix-layer-badge = "sema" > sema< / span > events
< span data-uix-layer-badge = "sema" > sema< / span > · events
< / h2 >
< p data-uix-section-desc >
Skeleton has no semantic events — it's a pure visual placeholder that the consumer unmounts
when real content is ready. The morfo declares no events, so the perceptual cascade has
nothing to react to. The single Provider part carries < code > aria-busy< / code > +
< code > role="status"< / code > for assistive tech.
Skeleton declares < strong > no semantic events< / strong > . It is a passive visual placeholder
the consumer unmounts when real content is ready — there is no commit/emerge/shift to
react to. Screen readers learn about its presence through the morfo's
< code > role="status"< / code > + < code > aria-busy="true"< / code > contract, not through sema.
< / p >
< p data-uix-section-desc >
If you need a perceptual cue when loading < em > starts< / em > or < em > completes< / em > , attach
the cue to the parent action (commit-submit on the form that triggered the fetch, or
commit-set on the slot that received the data) — not to Skeleton.
< / p >
< / section >
{ :else if tab === 'recipe' }
{ /if }
{ #if tab === 'recipe' }
< section data-uix-section >
< h2 data-uix-section-title > Recipe< / h2 >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > · canonical usage
< h2 data-uix-section-title > Eidos recipe< / h2 >
< p data-uix-section-desc >
Recipe lives in < code > src/uix/eidos/components/skeleton/skeleton.css< / code > . Tokens are
private (< code > --_skeleton-*< / code > ). The shimmer is a single keyframes block reused by
rect/circle/text variants.
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Selector< / th > < th > Owner< / th > < th > Purpose< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > < code > [data-skeleton]< / code > < / td >
< td > < span data-uix-tag data-kind = "morfo" > morfo< / span > < / td >
< td > Provider marker. Emitted by the component on the root div.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-skeleton][data-shape='rect'|'circle'|'text']< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Toggles border-radius + intrinsic aspect-ratio + layout.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-skeleton][data-size='xs'..'xl']< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Sets < code > --_skeleton-size< / code > . Default < code > md = 1rem< / code > .< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-skeleton][data-color='primary'..'loss']< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Color-mixes accent tracks into < code > --_skeleton-bg< / code > .< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-skeleton][data-animated]< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Adds the linear-gradient + < code > skeleton-shimmer< / code > keyframes.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-skeleton-line]< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > One bar inside < code > shape='text'< / code > . Stacks via flex column.< / td >
< / tr >
< tr >
< td class = "name" > < code > @media (prefers-reduced-motion: reduce)< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Stops the shimmer; placeholder shape stays visible.< / td >
< / tr >
< / tbody >
< / table >
< / div >
< pre data-uix-snippet >< code > { eidosSnippet } </ code ></ pre >
< / section >
{ :else if tab === 'a11y' }
{ /if }
{ #if tab === 'a11y' }
< section data-uix-section >
< h2 data-uix-section-title > Accessibility< / h2 >
< ul >
< li >
Renders < code > < div role="status" aria-busy="true"> < / code > . Screen readers will
announce the configured label ("Loading" in EN, "Cargando" in ES) when the skeleton
enters the DOM.
< / li >
< li >
When swapped with real content, the screen reader hears the new content because the
Skeleton element is removed entirely (not just hidden).
< / li >
< li >
< code > aria-label< / code > can override the default "Loading" string with a more specific
message (e.g. < code > aria-label="Loading user profile"< / code > ).
< / li >
< li >
Animation respects < code > prefers-reduced-motion< / code > — under reduced motion the shimmer
stops but the placeholder shape remains visible.
< / li >
< li >
Color contrast: the placeholder uses < code > color-mix< / code > at 50– 60% opacity over the
surface, low enough to read as "non-content" but high enough to register against most
backgrounds.
< / li >
< / ul >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Concern< / th > < th > Contract< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > Role< / td >
< td >
< code > role="status"< / code > — a polite live region. Screen readers announce the
element's label when it enters the DOM and stay silent otherwise.
< / td >
< / tr >
< tr >
< td class = "name" > Busy state< / td >
< td >
< code > aria-busy="true"< / code > — signals that the region's content is loading and
the user should expect a change soon.
< / td >
< / tr >
< tr >
< td class = "name" > Label< / td >
< td >
Defaults to the translated 'Loading' / 'Cargando'. Override with
< code > aria-label="Loading user profile"< / code > when you can give specific
context.
< / td >
< / tr >
< tr >
< td class = "name" > Keyboard< / td >
< td > Skeleton is not focusable. Tab order skips it entirely.< / td >
< / tr >
< tr >
< td class = "name" > Focus visible< / td >
< td > Skeleton does not paint a focus ring — no interactive state.< / td >
< / tr >
< tr >
< td class = "name" > Reduced motion< / td >
< td >
Shimmer animation freezes under
< code > prefers-reduced-motion: reduce< / code > . The placeholder shape stays visible
to preserve the loading affordance.
< / td >
< / tr >
< tr >
< td class = "name" > Color contrast< / td >
< td >
< code > color-mix< / code > at 50– 60% opacity over the surface — low enough to read
as "non-content" but high enough to register against most backgrounds.
< / td >
< / tr >
< tr >
< td class = "name" > Replacement< / td >
< td >
When swapped with real content, the skeleton element is removed entirely (not
just hidden). Screen readers then announce the new content via its own ARIA.
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
< / div >