jiplak
v1.0.5
Published
Pixel-perfect skeleton loading screens, auto-captured from your real React UI.
Maintainers
Readme
jiplak
Pixel-perfect skeleton loading screens, auto-captured from your real React UI.
Wrap a component. Run one command. Get a skeleton that matches it exactly.
English · Bahasa Indonesia
Why jiplak?
Hand-drawn skeletons drift out of sync with the real UI the moment a layout changes. jiplak ("to trace" in Indonesian) traces your rendered components in a real browser and stores their exact shape. You never draw a skeleton again.
- Pixel-exact. Every line of text becomes its own bar, sized to the glyphs. Images, buttons, and cards keep their real size and corner radius.
- Zero route list. The CLI crawls your app through its own links.
- Responsive. Captured at several screen widths; the right one is picked at runtime.
- One YAML file.
jiplak.yaml, one line to start. - Dark mode. Follows the
.darkclass (Tailwind convention). - Tiny runtime. One React component; the capture tooling never ships to the browser.
Contents
Quick start
1. Install and set up
npm install jiplak
npx jiplak initinit creates jiplak.yaml and an empty src/bones/registry.ts.
2. Import the registry once
In a client component that always loads, such as your providers:
'use client'
import '@/bones/registry' // or a relative path to src/bones/registry3. Wrap components that wait for data
import { Skeleton } from 'jiplak/react'
function Profile() {
const { data, isLoading } = useProfile()
return (
<Skeleton name="profile-card" loading={isLoading}>
<ProfileCard user={data} />
</Skeleton>
)
}4. Start your app, then capture
npm run devIn a second terminal:
npx jiplak buildDone. While loading is true, the skeleton shows automatically.
FAQ
jiplak copies exactly what it sees at build time. If your component is empty
or shows placeholder text without data, pass a fixture that looks like real
content: the same title length, the same number of summary lines, an image if
real rows have one. Then run npx jiplak build again.
No. Wrap only the parts that wait for data, such as cards, lists, or tables.
Static parts like headers and menus need nothing. One screen can hold several
<Skeleton> components with different names.
Yes. jiplak build crawls your app through its links, finds every
<Skeleton>, and records its shape. Your only job is to wrap components.
After a wrapped component changes its layout, or after you add a new
<Skeleton>. Commit the src/bones/ folder so your team does not need to
rebuild.
Your app works as usual. Components render normally, just without a skeleton.
Pass sample content through fixture. jiplak captures the fixture instead of
the real children:
<Skeleton name="profile-card" loading={isLoading} fixture={<ProfileCard user={sampleUser} />}>
<ProfileCard user={data} />
</Skeleton>Yes. Write glob paths in routes; the crawler then follows only links that
match one of them:
routes:
- / # plain path: a start point
- /dashboard/** # ** = every page below /dashboard
- /products/* # * = one dynamic segment; one sample page is enough- A glob also starts at its fixed prefix (
/products/*starts at/products), so you never write a real slug. - A
*glob is visited once: every/products/:idpage has the same shape, so jiplak captures the first one it finds and skips the rest. - Without globs, every link is followed, as before.
The crawler finds only pages that another page links to. Add unlinked pages as extra start points:
routes:
- /
- /settings/profileTo visit only the listed routes, turn crawling off:
crawl: falseLogout links and files (pdf, images, csv) are always skipped. Skip other
paths with ignore:
ignore:
- /adminThe crawler stops at 50 pages so dynamic routes (/post/1, /post/2, …)
cannot run forever. Raise the limit if needed:
maxPages: 200Log in once, then build as usual:
npx jiplak login # a browser window opens: sign in like a normal user, then press Enter
npx jiplak build # reuses that sessionThe session is saved to .jiplak/session.json; the .jiplak folder ignores
itself, so it never lands in git. build writes refreshed cookies back to it,
so it keeps working until the app ends the session. When a page redirects to
your sign-in page, build says so: run jiplak login again.
No browser window (CI)? Pass cookies through the environment instead, in the Cookie header format:
JIPLAK_COOKIES="session=your-session-cookie; other=value" npx jiplak buildUse the Chrome you already have:
channel: chromeOr download Chromium once:
npx playwright install chromiumConfiguration
Every key is optional. Values below are the defaults. Unknown keys fail with a clear error, so typos never pass silently.
# capture
url: http://localhost:3000 # your running app
routes: [/] # start points; globs (* and **) limit the crawl
crawl: true # false: visit only routes
ignore: [] # path prefixes never visited
maxPages: 50 # crawl limit
breakpoints: [375, 768, 1280] # screen widths to capture
out: ./src/bones # output folder
wait: 800 # ms to wait after page load
channel: chrome # optional: use installed Chrome
cookies: [] # optional: name=value (prefer the JIPLAK_COOKIES env var)
# look (props on <Skeleton> override these)
animate: pulse # pulse | shimmer | solid
color: "#ececec" # light mode
darkColor: "#262626" # dark mode (.dark class)
shimmerColor: "#f7f7f7"
darkShimmerColor: "#323232"
speed: 1.6sAPI
<Skeleton>
| Prop | Required | Description |
| --- | --- | --- |
| name | yes | Unique name: letters, digits, -, _ |
| loading | yes | true while data is loading |
| fixture | no | Sample content captured at build time |
| animate, color, darkColor, shimmerColor, darkShimmerColor, speed | no | Override jiplak.yaml for this skeleton |
| options | no | { exclude: ['.badge'] } skips elements; { leafTags: ['section'] } turns a tag into one bone |
| bones | no | Bones passed directly instead of the registry |
| className | no | Class for the wrapper element |
Functions
| Function | Description |
| --- | --- |
| registerBones(map) | Register bones by name. The generated registry calls it. |
| configureJiplak(theme) | Set global look defaults. The generated registry calls it with your YAML values. |
CLI
jiplak init # create jiplak.yaml and an empty registry
jiplak login [url] [--config jiplak.yaml] # log in once; build reuses the session
jiplak build [url] [--config jiplak.yaml] # capture every <Skeleton>How it works
jiplak buildopens your app in a headless browser, starting atroutes, and follows links on the same origin.- After the page and its web fonts load, every
<Skeleton>is captured at each breakpoint ("bones"):- each rendered line of text becomes one bar, measured from the glyphs; clamped or clipped lines are left out;
- images, icons, buttons, and inputs become blocks with their visible corner radius (including a radius that comes from a clipping parent);
- cards and panels with a background or border become faint surfaces drawn behind the blocks.
- Bones are saved to
src/bones/<name>.bones.json, plus aregistry.tsthat loads them. - At runtime, while
loadingistrue, the bones are drawn over the hidden component, using the breakpoint that fits the container width.
Development
bun install
bun test
bun run typecheck
bun run buildIssues and pull requests are welcome on GitHub.
Sponsor
jiplak is proudly sponsored by Lokio.
Like jiplak? Support its development:
Author
Wahyu Agus Arifin · @ItPohgero
License
MIT © 2026 Wahyu Agus Arifin
