How it works
Skullet does not guess at sizes: it lets the browser lay out the real content, then reads the result.
- Layout by the browser. Your content renders in place from the closest data there is — the sample, or the data a previous cycle remembered — with its text transparent and its media hidden. Every size and line break is the browser's own.
- Lines from the DOM. Each text node is measured with
Range.getClientRects(), which returns one box per line it wraps to. Images, controls and painted boxes such as buttons and chips are measured whole. - Heights from the font. A line box is taller than its letters, so each bar is trimmed
to the font's own metrics, read once per font with canvas
measureText: ascender to descender by default. - Drawn on top. The bars are absolutely positioned over the unseen content, in a shadow root the page's CSS cannot reach. Every bar shares one shimmer, timed by the document clock and its place on the page.
- Kept current. A
ResizeObserver, aMutationObserver, image loads and font loads trigger a re-measure.
First paint
The server renders the sample, so the layout is right from the first byte, but the bars are
drawn by JavaScript: between the first paint and the bundle running, a loading skeleton is
an empty box. <Skullet.Boot.boot/1 /> closes that gap by inlining priv/static/skullet.pre.js
— a self-contained bundle of the same measuring, metrics and shimmer code — as a
synchronous <script>. It paints before first paint, and the engine takes the bars over
when it starts.
Place it once per page, at the end of <body>; see usage
for the component and the CSP section for its nonce.
Working on the engine
The package needs no Node: the compiled engine is committed in priv/static. Changing it
needs pnpm and Node, from the repository root:
pnpm install --allow-build=esbuild # esbuild's postinstall is declined by default
pnpm run check # tsc (type check), oxlint, oxfmt --check
pnpm build # assets/js/*.ts → priv/static/skullet{.js,.min.js}, then skullet.pre.js
pnpm test # builds, then drives Playwright against assets/test/fixtures
oxfmt reads assets/.oxfmtrc.json.
priv/static holds bundles, not a file per module: one ESM bundle for bundlers, one
minified script for a <script> tag, both maps-free except the script build, plus the
hand-written skullet.d.ts. pnpm verify rebuilds and fails if the committed files would
change.
Before finishing a change
mix ci runs the whole quality gate — compile --warnings-as-errors, format --check-formatted, coveralls (the test suite, with line coverage and a 95% floor),
credo --strict, dialyzer, spec_lint --warnings-as-errors, ex_dna --max-clones 0 and
reach.check --arch --smells — set up by
VibeKit. mix coveralls.html writes the
per-line report to cover/.
The demo app
example/ is a Phoenix LiveView app using the component on a real assign_async, with a
button that pins the loading state:
cd example && mix deps.get && mix assets.setup && mix phx.server
It runs at http://localhost:4000; the deployed copy is
https://skullet-demo.fly.dev/. example/README.md covers what each card shows and how it
is deployed.