npm.io
0.2.1 • Published 2h ago

@iskeletor/core

Licence
MIT
Version
0.2.1
Deps
0
Size
231 kB
Vulns
0
Weekly
0

@iskeletor/core

The measurement engine behind iskeletor.

Give it an Element; get back JSON describing what the browser actually laid out, and DOM that stands in for it. No dependencies, no framework imports, no globals beyond the element's own document — which is what makes a React, Vue or Angular adapter a thin layer on top rather than a rewrite.

Most React apps want @iskeletor/react instead. Reach for this package directly when you have no framework, or when you are building an adapter for one.

npm install @iskeletor/core

Documentation and a live demo — switch the component on the page and watch the skeleton be re-measured in your browser.

Quick start

import { measureElement, renderSkeleton } from "@iskeletor/core";

const layout = measureElement(document.querySelector("#card"));
document.querySelector("#placeholder").append(renderSkeleton(layout));

skeletonFor(element) does both in one call.

A container with no box of its own — display: contents, the way a wrapper stays out of the layout — is measured as the area its children occupy, so you can measure live content without a wrapper that changes it.

Measuring content that is already on the page

Content on screen is not finished the moment it mounts. Images arrive late, and a component that swaps its own placeholder for the real thing once an image loads changes shape afterwards; measure into that window and the skeleton stands in for a half-loaded card rather than the finished one.

import { waitForContent, measureElement } from "@iskeletor/core";

await waitForContent(card);          // images, webfonts, then two steady frames
const layout = measureElement(card);

Bounded, like every other wait here: one image that never loads must not mean no measurement at all.

Measuring something that is not on screen yet

import { measureWithSandbox } from "@iskeletor/core";

const layout = await measureWithSandbox((container) => {
  container.innerHTML = template;
});

The sandbox mounts into a hidden, off-screen host, waits for layout and webfonts, measures, and always tears itself down. It hides with opacity, never visibility — which is inherited, and would make every measured child report itself as hidden.

Pass a viewport width to get a real nested viewport (an iframe), so CSS media queries resolve at that width:

import { measureResponsive, DEFAULT_BREAKPOINTS } from "@iskeletor/core";

const maps = await measureResponsive(mount, DEFAULT_BREAKPOINTS);
// { mobile: {...}, tablet: {...}, desktop: {...} }

Every wait inside the sandbox is bounded. Animation frames never fire in a backgrounded tab and webfonts can stall, and neither should leave a caller waiting forever for a measurement.

The layout map

{
  "version": 1,
  "width": 340, "height": 373,
  "children": [
    {
      "type": "container", "x": 0, "y": 0, "width": 340, "height": 373,
      "radius": "12px",
      "children": [
        { "type": "image", "x": 1, "y": 1, "width": 338, "height": 180, "radius": "" },
        {
          "type": "text", "x": 16, "y": 197, "width": 306, "height": 60,
          "lines": 3, "lineHeight": 20,
          "lineBoxes": [
            { "offset": 0, "width": 1 },
            { "offset": 0, "width": 0.98 },
            { "offset": 0, "width": 0.41 }
          ]
        }
      ]
    }
  ]
}

It is plain JSON on purpose: it can be cached, persisted, diffed, or generated ahead of time by a headless browser. LayoutCache serialises with toJSON() and reloads with hydrate(), and subscribe() reports entries as they arrive — so a placeholder that is waiting on a measurement taken elsewhere on the page can pick it up instead of sitting on its fallback for the rest of the session.

Node types are text, image, button, circle, rect and container. Coordinates are relative to the parent node, so a subtree can be moved without recomputing anything.

Repeats. Structurally identical siblings that are evenly spaced fold into one node carrying repeat: { count, strideX, strideY }. A run that wraps folds too, and carries columns and rowStride alongside them - so a grid of seven cards three across is one node, not one per row. repeatOffset(repeat, i) returns where the i-th repetition sits, whichever shape was recorded.

A layout that describes neither a line nor a grid - ragged rows, a centred last row - is left expanded rather than having positions invented for it.

Text. Lines are measured with Range.getClientRects(), which is the only way to learn how a string actually wrapped. lineBoxes records each line's extent as a ratio of the block, so a short heading in a wide container is drawn as a short bar - and centred text stays centred.

Overriding the guesses

The classifier is a heuristic. When it gets something wrong, say so in the markup:

<div data-skeleton-type="circle">JD</div>
<div data-skeleton-ignore>debug panel</div>

data-skeleton-type accepts any node type. data-skeleton-ignore (and aria-hidden="true") removes the element from the map entirely.

Theming

renderSkeleton(layout, {
  baseColor: "#e6e8eb",
  highlightColor: "#f4f6f8",
  duration: 1.4,      // seconds per shimmer sweep
  animate: true,      // honours prefers-reduced-motion regardless
  lineFill: 0.72,     // how much of a line box a text bar fills
  fluid: false,       // percentage widths instead of pinned pixels
});

Options are applied as CSS custom properties on the generated root, so several skeletons can be themed independently while sharing one stylesheet.

API

Export Purpose
measureElement(el, options?) Walk a live element into a layout map.
renderSkeleton(layout, options?) Build placeholder DOM.
renderSkeletonToString(layout, options?) Same, as markup.
skeletonFor(el, options?) Measure and render in one call.
createSandbox(options?) Off-screen host to mount into.
measureWithSandbox(mount, options?) Mount, settle, measure, clean up.
measureResponsive(mount, breakpoints, options?) One map per breakpoint.
waitForContent(el, options?) Wait until on-screen content is worth measuring.
LayoutCache, globalLayoutCache LRU cache keyed by id, props and viewport.
pickBreakpoint(width, breakpoints?) Widest breakpoint that fits.
repeatOffset(repeat, i) Where the i-th repetition sits.
ensureStyles(document), SKELETON_CSS Stylesheet installation.

Keywords