Splash
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/splash.jsonA desktop product has a second or two between the window appearing and the first screen being ready — a workspace to open, a database to migrate, a plugin to start — and a blank window for that long reads as a crash. So the window shows the product instead.
<Splash mark={<Mark />} name="kilna" tagline={t('app.tagline')} version={`v${__APP_VERSION__}`} status={t('splash.opening')} tip={t('splash.tip')}/>It is a status region. role="status" with a polite live region, so the
status line is announced when it changes and nothing about the panel asks
for an answer. It is not a dialog: the reader cannot act on it.
The sweep stops honestly under reduced motion. The theme halts every
animation for prefers-reduced-motion, which on its own would leave the sweep
parked off the end of its track — an empty bar. The bar is drawn full and
still instead, and the same when busy is false: waiting on a person has
no progress to show.
The keyframes travel with the component. A product installs the theme on
its first day and a splash once, so the sweep is a <style> the component
renders rather than a keyframe in the theme.
Only the lines you give are drawn. An empty <p> still takes its height,
and the picture below has no such line — the name would jump when React took
over.
The other half: before the bundle
Section titled “The other half: before the bundle”This component cannot be the first thing the window shows, because React is what is still loading. So the page paints the same picture in inline CSS — inline, because the stylesheet arrives with the bundle, which is the thing being waited for — and the component takes over on its first render with the same geometry, so nothing moves:
useEffect(() => { document.getElementById('splash')?.remove()}, [])The static half, in index.html. The two backgrounds are the theme’s dark
and light written out, since the theme file is in that same bundle; a stored
explicit theme is applied by the page a moment later and the splash follows it
through the classes on <html>:
<style> html { background: #141216; color: #e8e3ec; } @media (prefers-color-scheme: light) { html { background: #f7f5f9; color: #221c27; } } html.dark { background: #141216; color: #e8e3ec; } html.light { background: #f7f5f9; color: #221c27; } #splash { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; user-select: none; font-family: 'Segoe UI Variable Text', 'Segoe UI', system-ui, -apple-system, sans-serif; } #splash .mark { width: 56px; height: 56px; margin-bottom: 6px; } #splash .name { font-size: 26px; font-weight: 600; letter-spacing: 0.02em; line-height: 1; } #splash .tagline { font-size: 13px; opacity: 0.62; } #splash .version { margin-top: 6px; font-family: Consolas, ui-monospace, monospace; font-size: 11px; opacity: 0.45; } #splash .bar { position: relative; width: 160px; height: 2px; margin-top: 18px; border-radius: 999px; background: rgba(128, 128, 128, 0.18); overflow: hidden; } #splash .bar::after { content: ''; position: absolute; top: 0; left: -40%; width: 40%; height: 100%; border-radius: 999px; background: #d9569e; animation: splash-sweep 1.1s ease-in-out infinite; } @keyframes splash-sweep { to { left: 100%; } } @media (prefers-reduced-motion: reduce) { #splash .bar::after { animation: none; left: 0; width: 100%; } }</style>
<div id="splash" aria-hidden="true"> <svg class="mark" viewBox="0 0 32 32">…</svg> <div class="name">kilna</div> <div class="tagline">From raw idea to shipped work.</div> <div class="version">v__APP_VERSION__</div> <div class="bar"></div></div><div id="root"></div>The raw colours are allowed there and nowhere else: this is the one file that
runs before the theme exists. aria-hidden because the React half is the one
that speaks; two status regions saying the same thing would be announced
twice.
Props
Read from the types in ui/splash.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.
| Prop | Type | Default | |
|---|---|---|---|
mark | ReactNode | The product's mark, drawn at 56px. | |
namerequired | string | The product's name. | |
tagline | string | The line under the name. | |
version | string | The version, drawn in the mono face; the v is the product's to add. | |
status | string | What the application is doing right now. | |
tip | string | One thing worth knowing while it does it. | |
busy | boolean | true | Whether the bar is sweeping. Still and full when the application is waiting on something that has no progress, such as a person. |
And every prop of React's HTMLAttributes<HTMLDivElement> except children, passed on.
Accessibility
The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 14 rules applied and passed, and none failed. The accessibility report has every item.
Left to a person, where axe could not decide: color-contrast.
History
Each release that changed ui/splash.tsx - the file a product copies - newest first.