registry:ui · Layout
boot-splash — the mask whose styles cannot be late
The FOUC mask (Owner design, 2026-09-19): a splash layer whose styles CANNOT be late — every style rides the HTML itself (inline style attributes + a <style> block carried inside <svelte:head>, which SSR renders INLINE into the document — never an async chunk). It paints before any stylesheet and masks the unstyled window (async css delivery, the dev lane's JS-fetcher stylex css, slow-network waterfalls, late fonts). Slots: logo, loading (default: a hardcoded SMIL SVG spinner — zero css by construction; compose a spin-pack loader here), title, subtitle, description (progress or time-consuming copy). Exit vocabulary: opacity-out | blur-out | none (keyframes in the same head block; reduced-motion skips; a <noscript> rule never traps the page). Dismissal: revealOn fonts | load | manual, with a timeoutMs cap and a minMs display floor. website-scaffold carries the splash seat at its host root.
$npx jixoai-ui add boot-splash
https://ui.jixoai.com/r/boot-splash.json
overview
Overview
The FOUC mask: a fixed, z-max layer whose every style rides the HTML itself, covering the one window stylesheets can still be late for — then dismissing itself on a signal you choose, under a cap you control.
The problem it masks: styles arrive asynchronously — the built head carries ~20
render-blocking stylesheet links (a waterfall on slow networks), and a dev lane's css
rides a JS fetcher module (a guaranteed bare flash). Whatever the cause, the first
paint can be wrong. The splash covers that window with a layer whose styles CANNOT be late: element styles ride inline style
attributes, and the sheet-level vocabulary — the light/dark grounds, the exit
keyframes, the reduced-motion and noscript escapes — rides a style block inside svelte:head, which SSR renders inline into the
document. The component imports no css, no atoms, no tokens.
The dismissal economy: revealOn picks the signal
(fonts — the last layout-shifting resource class · load · manual), capped by timeoutMs so a hung font CDN never holds the
page hostage, floored by minMs so an instant
dismissal never blinks, and timed by durationMs through one of the two exit keyframes. The mask is visual-only — pointer-events: none — so a fast click passes
through to the page beneath.
The eight-axis surface rides whatever window the layer has: the carriers stamp inline (the component's own element idiom — the zero-css law untouched), and the layer is transient by design. Six of the eight axes find no reader on a surface with no atoms, no corners and no shadows; size stamps a real echo with a rem caveat; theme stamps a class the grounds cannot hear — the grounds answer the host's own vocabulary instead. Per-axis, measured, below.
usage
Usage
Mount it inside WebsiteScaffold's splash snippet — the scaffold carries the seat at its host root, the splash renders before the page chrome and dismisses itself when the sheet settles.
<script lang="ts">
import WebsiteScaffold from '@ui/website-scaffold';
import BootSplash from '@ui/boot-splash';
</script>
<WebsiteScaffold>
{#snippet splash()}
<BootSplash
title="your-app"
subtitle="the design language"
description="loading the style sheet…"
exit="blur-out"
/>
{/snippet}
<!-- the page itself -->
</WebsiteScaffold>boot-splash
Replay the mask: the layer covers the viewport (fixed, z-max, pointer-events none — clicks pass through), holds through the display floor, then runs the chosen exit. This page's fonts are already settled, so 'fonts' and 'load' both dismiss after the 350ms floor; 'manual' holds until you dismiss it.
The layer you just saw IS the component: hardcoded grounds (light
and prefers-color-scheme dark pairs), the default
SMIL arc spinner, and the exit keyframes — all carried in the
document itself. Nothing about it can arrive late.
<script lang="ts">
import BootSplash from '@ui/boot-splash';
</script>
<BootSplash
bind:open={splashOpen}
exit="blur-out"
revealOn="fonts"
title="your-app"
description="loading the style sheet…"
/>the law
The zero-css-file law
The component imports no css, no atoms, no tokens. Element styles live in inline style attributes; the sheet-level vocabulary — light/dark grounds, the exit keyframes, the reduced-motion and noscript escapes — rides a style block inside svelte:head, which SSR renders inline into the document. Any external stylesheet would reintroduce the very race this layer exists to hide.
<!-- what the served HTML carries (view-source on this site's own boot): -->
<style data-jx-boot-splash>
.jx-boot-splash-layer { position: fixed; inset: 0; z-index: 2147483647; … }
@media (prefers-color-scheme: dark) { … }
@keyframes jx-boot-splash-blur-out { to { opacity: 0; filter: blur(14px); } }
</style>
<noscript><style>.jx-boot-splash-layer { display: none !important; }</style></noscript>
<div data-jx-splash="layer" class="jx-boot-splash-layer" role="status" …>
<!-- the SMIL spinner: animateTransform rides the SVG itself -->
</div>slots
The five slots
logo (the brand mark) · loading (the animation — default: a hardcoded SMIL arc spinner, zero css by construction; compose a spin-pack loader here) · title · subtitle · description (progress or time-consuming copy). Text slots are plain strings; the two composition slots are snippets. A logo takes the lane — the brand mark IS the liveness signal, so the default spinner stands down under it.
<BootSplash
title="your-app"
subtitle="the terminal design language"
description="loading the style sheet…"
exit="blur-out"
>
{#snippet logo()}
<svg width="40" height="40" viewBox="0 0 40 40" aria-hidden="true"><!-- your mark --></svg>
{/snippet}
{#snippet loading()}
<!-- any spin-pack loader: its svg carries its own inline styles -->
{/snippet}
</BootSplash>exits
Exit vocabulary — opacity-out · blur-out · none
The keyframes live in the same head block, timed by --jx-boot-splash-duration; the animationend hands control back (open flips false, the layer unmounts). prefers-reduced-motion skips the animation; the manual mode's bind:open flip rides the identical leaving phase; exit='none' needs no phase at all — the floor elapses and the layer is gone.
exit="opacity-out" → to { opacity: 0 }
exit="blur-out" → to { opacity: 0; filter: blur(14px) }
exit="none" → no phase: the floor elapsed, the layer unmountsapi
API
Eleven served props; zero required. The dismissal economy: revealOn picks the signal, timeoutMs caps it, minMs floors it, durationMs times the exit. (meta arithmetic: 21 props − 8 ambient axes = 13 family seats — the 11 served rows plus rest/style, which ride the anchor-passthrough convention.)
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| logo | Snippet | — | The brand mark (LOGO slot) — any inline svg/mark; it rides the layer’s own centering. |
| loading | Snippet | SMIL arc spinner | The loading animation slot. The default is a hardcoded SMIL svg spinner (animateTransform — zero css by construction); compose a spin-pack loader here, its svg carries its own inline styles. |
| title | string | — | The primary line; also the status region’s aria-label fallback. |
| subtitle | string | — | The secondary line. |
| description | string | — | Running copy — progress, or time-consuming text (the Owner spec’s consumption lane). |
| exit | 'opacity-out' | 'blur-out' | 'none' | 'opacity-out' | The exit animation; keyframes ride the same head block. |
| revealOn | 'fonts' | 'load' | 'manual' | 'fonts' | When the layer dismisses itself: document.fonts.ready (the last layout-shifting resource class), the window load event, or manual — bind:open is yours (a flip to false runs the exit vocabulary). |
| timeoutMs | number | 4000 | The hard cap on the auto signals (fonts/load) — a hung font CDN never holds the page hostage. Manual is uncapped: the cap never arms, your bind:open is the only exit (live-proven present past a 4s cap). |
| minMs | number | 350 | The minimum display floor — avoids the blink-flash of an instant dismissal. |
| durationMs | number | 350 | The exit animation length. |
| open | boolean (bindable) | true | The dismissal state; manual mode’s control surface. BIND it — a one-way open={state} wire is the dead-replay trap: the child’s internal dismissal writes never reach your state. |
axes
The eight axes on boot-splash
A FIRST-TIME all-no-own contract over a TRANSIENT surface: the carriers stamp inline (the component's own element idiom — the zero-css-file law untouched) and the layer reads nothing — no kernel channels, no tokens, no corners, no shadows (grep receipts). The grounds are the hardcoded head-block pairs that answer the HOST's vocabulary (prefers-color-scheme + the ancestor .dark bridge); size stamps a real echo whose rem-tier copy holds the root scale; the exit runs on the family's own duration channel with the reduced-motion kill. Measured 2026-09-23; the layer is transient — every receipt names its window.
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 |
|---|---|---|---|
| density | 'small' | 'medium' | 'large' | 'auto' | number · query() | 'auto' | CARRIER-ONLY — the stamp lands inline on the layer and nothing reads it: the zero-css-file law means no atoms and no channel reads exist (grep receipt: the head block and the markup read no var(--jx-*) token — the only var in the family is its own --jx-boot-splash-duration). The transient mask consumes nothing. Number unit: coefficient. |
| size | 'small' | 'medium' | 'large' | 'auto' | number · query() | 'auto' | THE §11 ECHO WITH A REM CAVEAT — the mirror lands verbatim on the layer (--jx-size-effective: 18px; font-size: var(--jx-size-effective, 1rem) measured live) and the layer's own font-size follows it; but the copy tiers are REM (title 1rem · subtitle 0.8125rem · desc 0.75rem — document-root-relative), so the built-in text holds the root scale and only consumer-authored em copy inside the slots would scale. The stamp is real; the visible follow-through is the consumer's. Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' · query() | 'auto' | SUPPLY-ONLY — the layer is full-viewport (position: fixed; inset: 0): corner geometry has no seat (grep receipt: zero border-radius declarations in the family). Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number · query() | 'auto' | SUPPLY-ONLY — the mask has no corners (grep receipt: zero border-radius reads); the stamps land on the layer and evaporate with it. Number unit: px. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string · query() | 'auto' | SUPPLY-ONLY — the ink and ground are the HARDCODED head-block oklch pairs: the one surface in the system that may not read a token (the zero-css-file law — token-dependence IS the race this layer masks). --jx-color-effective stamps and finds no reader (grep receipt). Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' · query() | 'auto' | STAMP-ONLY ON SELF; THE GROUNDS ANSWER THE HOST — class:dark lands on the layer itself, but the head block's bridge selectors (.dark .jx-boot-splash-layer) match an ANCESTOR, so the prop's own class paints nothing (measured live: theme="dark" stays light on a light host). The grounds flip through prefers-color-scheme and the ancestor .dark class bridge (measured: an ancestor .dark turns the ground oklch(1 0 0) → oklch(0.145 0 0) and the ink → oklch(0.9551 0 0)). The prop cannot repaint the mask — the head block renders once, before any prop exists; host vocabulary is the only voice a pre-paint layer can hear. No number lane. |
| elevation | 'level-1' | 'level0'…'level5' | 'auto' | number · query() | 'auto' | SUPPLY-ONLY — the mask casts no shadow: it IS the ground (grep receipt: zero shadow declarations in the family). Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number · query() | 'auto' | THE EXIT IS ITS OWN MACHINE — the exit animation reads the family's own --jx-boot-splash-duration channel (durationMs, default 350ms), not the motion kernel; prefers-reduced-motion kills the exit outright on the MANUAL path (measured: a 0ms instant unmount), while auto+RM keeps the 350ms floor as the blink-flash guard — the floor survives RM, only the exit phase dies (both paths measured); zero transition declarations (grep receipt). Number unit: coefficient. |
boot-splash · the axes, live
Three receipts on one stage. Size 18: the mirror lands verbatim on the layer (--jx-size-effective: 18px; the layer's font-size follows) while the rem-tier copy holds the root scale. The theme pair is controlled: the prop ALONE (theme='dark', no host class) stays light — its class lands on the layer itself, where no bridge selector can hear it — while the ANCESTOR .dark host flips the grounds to the dark pair (oklch(0.145 0 0) / oklch(0.9551 0 0)) with no prop at all.
<BootSplash
size={18}
revealOn="manual"
bind:open={sizeSplashOpen}
title="size 18"
description="the layer's font-size follows the mirror"
/>
<!-- theme, controlled: the prop ALONE paints nothing — the grounds
answer the HOST's vocabulary (an ancestor .dark flips them) -->
<BootSplash theme="dark" revealOn="manual" bind:open={propSplashOpen} title="prop alone" />
<div class="dark">
<BootSplash revealOn="manual" bind:open={darkSplashOpen} title="host bridge" />
</div>| Token | Default | Source |
|---|---|---|
| --jx-boot-splash-duration | durationMs · 350ms | component |
| grounds | light oklch(1 0 0)/oklch(0.2 0 0) · dark 0.145/0.9551 | structural |
| --jx-boot-splash-mark | oklch(0.2 0 0 / 0.85) · white/0.85 on dark | structural |
| text tiers | 1rem · 0.8125rem · 0.75rem | structural |
| kernel channels | unread | density |
a11y
Accessibility
The layer is role=status (labelled by the title) — a polite loading announcement. It is pointer-events: none by law: a fast click during the boot window passes through to the (unstyled but functional) page beneath. noscript hides the layer entirely; reduced-motion skips the exit; the timeout cap guarantees the page is never held hostage.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| role | "status" | component-owned | A polite live region — screen readers announce the loading state once, without interrupting. |
| aria-label | string | the title prop (fallback "loading") | Names the status region. |
| pointer-events | none | always | The mask is visual-only — it can never eat input, so the unstyled page beneath stays operable through the boot window. |
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) |