CI codecov Elixir License: MIT Hex.pm

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:

filewhat it is
skullet.jsthe engine as one ES module — what a bundler imports
skullet.min.jsthe same engine as a script, with its source map: it defines window.Skullet (attach, setDebug, registerReveal, SkulletHook)
skullet.pre.jsthe pre-paint script Skullet.Boot inlines into the page, not a file you load
skullet.cssthe 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-src for the boot script above, which is inlined;
  • style-src for the theme, which is emitted as a <style> block rather than a style attribute, 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.

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.