Current section
Files
Jump to
Current section
Files
README.md
# Skullet
[](https://github.com/gilbertwong96/skullet/actions/workflows/ci.yml)
[](https://codecov.io/gh/gilbertwong96/skullet)
[](https://elixir-lang.org)
[](https://opensource.org/licenses/MIT)
[](https://hex.pm/packages/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](https://skullet.lochie.me/docs) to Phoenix LiveView.
**[Live demo](https://skullet-demo.fly.dev/)** — every option on one page, running the
example app in this repo.
## Installation
```elixir
def deps do
[
{:skullet, "~> 0.1.0"}
]
end
```
Two lines of wiring, in `assets/js/app.js` and `assets/css/app.css`:
```js
import { SkulletHook } from "../../deps/skullet/priv/static/skullet.js"
let liveSocket = new LiveSocket("/live", Socket, { hooks: { Skullet: SkulletHook } })
```
```css
@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`:
```html
<link rel="stylesheet" href="/assets/skullet.css">
<script src="/assets/skullet.min.js"></script>
<script>
Skullet.attach(document.querySelector(".skullet"))
</script>
```
## The first skeleton
```heex
<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.
```heex
<Skullet.Boot.boot nonce={@csp_nonce} />
<Skullet.provider theme={%{radius: "6px"}} nonce={@csp_nonce}>
…
</Skullet.provider>
```
[usage](guides/usage.md#content-security-policy) has the plug, the session hop and the rest.
## Where to read next
[Usage](guides/usage.md) covers the data sources, attributes, slots and the empty and error
states; [theming](guides/theming.md) the custom properties and the provider that sets them
in one place; [reveals](guides/reveals.md) how content replaces its skeleton; and
[internals](guides/internals.md) 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](CHANGELOG.md).