Skullet
Draws a skeleton of exactly what is about to load.
Your own component renders from sample data with its text transparent; the browser lays it out for real; rounded bars are drawn over it — one per wrapped line of text, one block per image or control — in a shadow root. Nothing is guessed, so nothing moves when the content arrives. A port of Skullet to Phoenix LiveView.
Live demo — every option on one page, running the example app in this repo.
Installation
def deps do
[
{:skullet, "~> 0.1.0"}
]
end
Two lines of wiring, in assets/js/app.js and assets/css/app.css:
import { SkulletHook } from "../../deps/skullet/priv/static/skullet.js"
let liveSocket = new LiveSocket("/live", Socket, { hooks: { Skullet: SkulletHook } })
@import "../../deps/skullet/priv/static/skullet.css";
With a path: or git: dependency there is no deps/skullet directory — point the two
imports at that checkout instead. The CSS file is not optional: it is what keeps the sample
invisible before the engine runs.
priv/static ships two bundles, their pre-paint script and the stylesheet; only the first
two are yours to import:
| file | what it is |
|---|---|
skullet.js |
the engine as one ES module — what a bundler imports |
skullet.min.js |
the same engine as a script, with its source map: it defines window.Skullet (attach, setDebug, registerReveal, SkulletHook) |
skullet.pre.js |
the pre-paint script Skullet.Boot inlines into the page, not a file you load |
skullet.css |
the stylesheet |
No build step? Copy skullet.min.js and skullet.css where your static files live and use
the engine off window.Skullet:
<link rel="stylesheet" href="/assets/skullet.css">
<script src="/assets/skullet.min.js"></script>
<script>
Skullet.attach(document.querySelector(".skullet"))
</script>
The first skeleton
<Skullet.skeleton :let={user} id="profile" async={@user} sample={@sample_user}>
<Profile.card user={user} />
</Skullet.skeleton>
While @user is loading, Profile.card is rendered from @sample_user and measured; when
the assign resolves, the skeleton reveals the real card in its place. sample is required
because it is the only data a dead render has, and id because the hook needs one and a
generated one would change every render.
Render <Skullet.Boot.boot /> once in the layout and that first skeleton is drawn before
the browser paints it, rather than after the bundle loads.
Under a Content Security Policy, Skullet wants a nonce in two places, and neither of them
is 'unsafe-inline':
script-srcfor the boot script above, which is inlined;style-srcfor the theme, which is emitted as a<style>block rather than astyleattribute, since a nonce rescues the element and nothing rescues the attribute.
<Skullet.Boot.boot nonce={@csp_nonce} />
<Skullet.provider theme={%{radius: "6px"}} nonce={@csp_nonce}>
…
</Skullet.provider>
usage has the plug, the session hop and the rest.
Where to read next
Usage covers the data sources, attributes, slots and the empty and error states; theming the custom properties and the provider that sets them in one place; reveals how content replaces its skeleton; and internals how measuring works and how the engine is built.
API reference: Skullet.skeleton/1, Skullet.provider/1, Skullet.box/1,
Skullet.Boot.boot/1, Skullet.Memory. Changes are in the
changelog.