npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ghostframes/core

v0.1.0

Published

> Framework-agnostic analysis engine, blueprint system, animation utilities, and shared types powering Ghostframes.

Readme

@ghostframes/runtime Runtime APIs

Framework-agnostic analysis engine, blueprint system, animation utilities, and shared types powering Ghostframes.

npm version bundle size license

The @ghostframes/core package is the framework-agnostic engine. It contains no React, Vue, or any UI-framework code. If you are building a React app, you almost certainly want @ghostframes/react, which includes the React facade.

Use @ghostframes/runtime directly when you are:

  • Building your own framework adapter (Vue, Svelte, Solid, etc.)
  • Generating blueprints server-side (Node.js / SSR — no DOM required)
  • Accessing low-level utilities such as the blueprint cache, structural hashing, or the animation system

Installation

# npm
npm install @ghostframes/runtime

# pnpm
pnpm add @ghostframes/runtime

# yarn
yarn add @ghostframes/runtime

Note: This package ships both CJS (dist/index.js) and ESM (dist/index.mjs) builds with full TypeScript declarations. It has zero runtime dependencies.


What is a Blueprint?

A Blueprint is a serializable tree of BlueprintNodes that describes what skeleton elements to render and where — without any DOM or framework code involved. Both the static and dynamic analyzers produce Blueprints.

type Blueprint = {
  version: number;         // Schema version — bump on breaking changes
  rootWidth: number;       // Width of the measured root element (px)
  rootHeight: number;      // Height of the measured root element (px)
  nodes: BlueprintNode[];  // Top-level children of the root
  structuralHash?: string; // djb2 hash of the DOM structure (for cache invalidation)
  generatedAt: number;     // Date.now() timestamp
  source: "static" | "dynamic";
};

Each BlueprintNode describes one skeleton block:

type BlueprintNode = {
  id: string;
  role: SkeletonRole | "container" | "table-row" | "table-cell";
  width: number;           // px
  height: number;          // px
  top: number;             // px, relative to the skeleton root
  left: number;            // px, relative to the skeleton root
  layout: Partial<LayoutProps>;
  text?: TextMeta;         // defined when role === "text" or "table-cell"
  borderRadius: string;    // CSS value (e.g. "4px", "50%")
  aspectRatio?: string;    // CSS aspect-ratio (e.g. "16/9")
  tagName: string;         // Uppercase HTML tag name
  slotKey?: string;        // Populated when role === "custom"
  children: BlueprintNode[];
  // Table flags
  isTable?: boolean;
  isTableRow?: boolean;
  isTableCell?: boolean;
};

Exports at a Glance

import {
  // Static analysis (SSR-safe, no DOM)
  generateStaticBlueprint,
  STATIC_DEFAULTS,
  type VNode,

  // Dynamic analysis (browser only)
  generateDynamicBlueprint,

  // Role inference
  inferRole,

  // Blueprint caching
  blueprintCache,
  computeStructuralHash,
  djb2,

  // Animation system
  animationSystem,
  AnimationSystem,

  // Types
  type Blueprint,
  type BlueprintNode,
  type SkeletonRole,
  type AnimationMode,
  type AnimationPreset,
  type SkeletonAnimationDefinition,
  type SkeletonConfig,
  type GhostframesPropsBase,
  type LayoutProps,
  type TextMeta,
  type MeasuredNode,
  type RelevantComputedStyles,

  // Constants
  DEFAULT_CONFIG,
} from "@ghostframes/runtime";

Static Analyzer — generateStaticBlueprint

Signature:

function generateStaticBlueprint(rootVNode: VNode): Blueprint

Traverses a virtual DOM tree (a React element tree, or any duck-typed { type, props } object) and produces a Blueprint without touching the DOM. This makes it safe to run in Node.js, during SSR, or inside Server Components.

How it works

  1. Recursively walks VNode nodes.
  2. Skips elements with data-no-skeleton or data-skeleton-ignore props.
  3. Unrolls fragments and non-HTML components (anything whose type is not a string) by visiting their children.
  4. Infers a SkeletonRole from the HTML tag name (e.g. img → image, button → button, p → text).
  5. Respects data-skeleton-role and data-skeleton-slot props as explicit overrides.
  6. Reads dimensions from (in priority order): data-skeleton-w/data-skeleton-hstyle.width/style.heightprops.width/props.heightSTATIC_DEFAULTS.
  7. Extracts layout props (display, flexDirection, gap, padding, etc.) from style for container nodes.

Usage

import { generateStaticBlueprint } from "@ghostframes/runtime";

// Works in Node.js — no DOM, no browser APIs
const blueprint = generateStaticBlueprint({
  type: "div",
  props: {
    style: { display: "flex", gap: "12px" },
    children: [
      {
        type: "img",
        props: { style: { width: 48, height: 48, borderRadius: "50%" } },
      },
      {
        type: "div",
        props: {
          children: [
            { type: "h2", props: { children: "Name" } },
            { type: "p",  props: { children: "Bio text here" } },
          ],
        },
      },
    ],
  },
});

When using with React JSX, the JSX element IS a valid VNode, so you can pass JSX directly:

// Works in a React Server Component or any tsx file
import { generateStaticBlueprint } from "@ghostframes/runtime";

const blueprint = generateStaticBlueprint(
  <div style={{ display: "flex", gap: "12px" }}>
    <img style={{ width: 48, height: 48, borderRadius: "50%" }} />
    <div>
      <h2>Name</h2>
      <p>Bio text here</p>
    </div>
  </div>
);

Return value

Returns a Blueprint with source: "static" and rootWidth: 0 / rootHeight: 0 (static mode uses flow layout; absolute positions are not computed).

STATIC_DEFAULTS

The fallback dimensions used when no explicit size is provided:

| Role | Default Width | Default Height | |----------|--------------|----------------| | text | 200px | 16px | | image | 300px | 200px | | avatar | 40px | 40px | | icon | 24px | 24px | | button | 120px | 36px | | input | 240px | 40px | | video | 400px | 225px | | canvas | 300px | 150px | | badge | 60px | 22px |

Override defaults per-element using data-skeleton-w / data-skeleton-h:

<img data-skeleton-w={96} data-skeleton-h={96} />

VNode type

type VNode = {
  type: unknown;                    // string for HTML tags, function/object for components
  props?: Record<string, unknown>;  // standard props including children
};

Dynamic Analyzer — generateDynamicBlueprint

Signature:

async function generateDynamicBlueprint(
  root: HTMLElement,
  config?: SkeletonConfig,      // defaults to DEFAULT_CONFIG
  options?: {
    include?: ElementMatcher[];
    exclude?: ElementMatcher[];
    budgetMs?: number;
  }
): Promise<Blueprint>

Measures a live DOM subtree and produces a pixel-precise Blueprint using absolute positions. This is the engine behind AutoSkeleton in @ghostframes/runtime.

Browser only. Requires window, document, getComputedStyle, and getBoundingClientRect.

Include/Exclude controls

Both analyzers accept optional include and exclude matcher arrays. exclude always wins over include.

import { generateDynamicBlueprint } from "@ghostframes/core";

const blueprint = await generateDynamicBlueprint(root, undefined, {
  include: [{ selector: ".skeleton-target" }],
  exclude: [{ selector: ".skeleton-ignore" }],
});

DOM attributes are also supported:

  • data-skeleton-include
  • data-skeleton-exclude

data-skeleton-exclude has highest precedence.

budgetMs is optional and enables a partial-traversal fallback: when the analyzer exceeds the configured budget, it returns the nodes computed so far instead of throwing.

Three-pass architecture

The function is carefully designed to avoid layout thrashing:

| Pass | What happens | DOM access | |------|-------------|------------| | 1 — Collect | Walk the DOM tree, gather element references and depth | Read-only structural walk | | 2 — Read | Batch-read all getBoundingClientRect() + getComputedStyle() calls in one loop | Read-only, no writes | | 3 — Process | Pure JS: infer roles, clip to overflow containers, build tree | Zero DOM access |

What gets skipped

  • Elements with data-skeleton-ignore or data-no-skeleton attributes
  • display: none (inline style fast-path, then computed style)
  • visibility: hidden or opacity < 0.01
  • Elements with position: fixed
  • Elements with width or height < 1px
  • Closed <details> elements
  • Shadow DOM boundaries (shadowRoot stops traversal)
  • Nodes deeper than config.maxDepth (default: 12)

Special handling

  • Overflow clipping: Nodes inside overflow: hidden parents are clamped to the parent's bounds.
  • CSS transforms: Elements with an active transform fall back to offsetTop/offsetLeft (layout-based) rather than getBoundingClientRect for position, avoiding transform-offset confusion.
  • Text metrics: For text role nodes, computes the number of lines (ceil(height / lineHeight)) and line height.
  • Table cells: <td> and <th> get table-cell role with their own bar sizing based on tableCellBarHeightRatio.
  • Virtualized lists: Logs a console.warn if a potential virtualized list is detected (very tall scrollHeight vs. few children).
  • Font loading: Awaits document.fonts.ready before measuring, ensuring text layout is correct when web fonts are used.

Usage

import { generateDynamicBlueprint, DEFAULT_CONFIG } from "@ghostframes/runtime";

const root = document.getElementById("my-card")!;
const blueprint = await generateDynamicBlueprint(root, {
  ...DEFAULT_CONFIG,
  maxDepth: 8,
  animation: "pulse",
});

console.log(blueprint.rootWidth, blueprint.rootHeight);
console.log(blueprint.nodes); // tree of BlueprintNodes

Return value

Returns a Blueprint with source: "dynamic", accurate rootWidth/rootHeight, and a structuralHash for cache invalidation.


Role Inferencer — inferRole

Signature:

function inferRole(node: MeasuredNode, config: SkeletonConfig): SkeletonRole

Determines the SkeletonRole for a single element using a weighted scoring system. Called internally by generateDynamicBlueprint — you can use it directly if you are building a custom analyzer.

Priority order

  1. Attribute overrides (exempt from scoring):

    • data-skeleton-ignore"skip"
    • data-skeleton-role="<role>" → explicit role (validated against known roles)
    • data-skeleton-slot"custom"
  2. Visibility checks:

    • display: none, visibility: hidden, width/height === 0"skip"
  3. Scoring rules — all matching rules accumulate points per role; the role with the highest score above threshold 30 wins.

Scoring rules overview

| Role | Triggers | |------|---------| | image | <img>, <picture>, elements with background-image, large empty elements | | avatar | Circular <img> (+150pts), circular square element 24–128px (+100pts) | | icon | <svg>iconMaxSize (+100pts), small square <svg> (+60pts), small square non-svg (+40pts) | | video | <video> (+100pts) | | canvas | <canvas> (+100pts) | | button | <button> (+100pts), role="button" (+80pts), short <a> (+60pts) | | input | <input>, <textarea>, <select> (+100pts), role="textbox"/"combobox" (+80pts) | | badge | Small pill with short text (height ≤ 28, width ≤ 120, borderRadius ≥ 20%) (+80pts) | | text | Text tags (<p>, <h1><h6>, <span>, etc.) (+80pts), non-empty leaf nodes (+40pts) |

MeasuredNode input structure

type MeasuredNode = {
  tagName: string;                          // Uppercase, e.g. "IMG", "BUTTON"
  ariaRole: string | null;                   // aria role attribute
  classList: string[];
  dataAttributes: Record<string, string>;    // All data-* attributes
  computedStyles: RelevantComputedStyles;
  rect: { width: number; height: number; top: number; left: number };
  hasChildren: boolean;
  childCount: number;
  textContent: string;                       // Trimmed inner text
  naturalWidth: number;                      // From HTMLImageElement.naturalWidth
  naturalHeight: number;
  src: string;                               // currentSrc, src, or href
};

Usage

import { inferRole, DEFAULT_CONFIG } from "@ghostframes/runtime";

const role = inferRole(measuredNode, DEFAULT_CONFIG);

Blueprint Cache

blueprintCache (singleton)

A pre-created instance of BlueprintCache. Use this in your adapter to avoid re-measuring when the DOM structure hasn't changed.

import { blueprintCache, computeStructuralHash } from "@ghostframes/runtime";

const root = document.getElementById("my-widget")!;
const hash = computeStructuralHash(root, 12);

// Attempt cache hit
const cached = blueprintCache.get(root, hash);
if (cached) {
  // Structure hasn't changed — use cached blueprint
  return cached;
}

// Cache miss — measure and store
const fresh = await generateDynamicBlueprint(root);
blueprintCache.set(root, fresh, hash);

BlueprintCache class

All methods operate on Element keys stored in a WeakMap, so blueprints are automatically garbage-collected when the element is removed from the DOM.

| Method | Signature | Description | |--------|-----------|-------------| | get | (el: Element, hash: string) => Blueprint \| null | Returns the cached blueprint if the structural hash matches; otherwise deletes the stale entry and returns null. | | set | (el: Element, blueprint: Blueprint, hash: string) => void | Stores a blueprint with its structural hash. | | invalidate | (el: Element) => void | Forcefully clears the cached blueprint for an element. |

computeStructuralHash

Signature:

function computeStructuralHash(root: Element, maxDepth?: number): string

Generates a djb2 hash of the DOM subtree structure. The hash encodes tagName, childElementCount, depth, data-skeleton-role, and data-skeleton-slot for each node — but ignores text content and other attributes. This means:

  • Adding or removing elements invalidates the cache ✅
  • Changing text content or non-skeleton attributes does not invalidate the cache (by design)
import { computeStructuralHash } from "@ghostframes/runtime";

const hash = computeStructuralHash(root);       // default maxDepth: 12
const hash2 = computeStructuralHash(root, 6);   // shallower, cheaper

djb2

Signature:

function djb2(str: string): string

The underlying fast non-cryptographic hash used by computeStructuralHash. Returns a hex string.

import { djb2 } from "@ghostframes/runtime";

djb2("hello world"); // "4a17b156"

Animation System

animationSystem (singleton)

A pre-created instance of AnimationSystem. Used by framework adapters to inject and manage the required CSS into <head>.

import { animationSystem, DEFAULT_CONFIG } from "@ghostframes/runtime";

// Inject CSS (idempotent — safe to call on every render)
animationSystem.injectStyles(DEFAULT_CONFIG);

// Clean up (e.g. on unmount of the last skeleton instance)
animationSystem.removeStyles();

AnimationSystem class

| Method | Signature | Description | |--------|-----------|-------------| | injectStyles | (config?: SkeletonConfig) => void | Injects a <style id="ghostframes-animations"> tag into <head>. Idempotent — only updates if the config changes. No-ops in non-browser environments. | | removeStyles | () => void | Removes the managed style tag from <head>. |

Injected CSS

injectStyles generates and injects:

CSS Custom Properties (on :root):

| CSS Variable | Config property | Default | |--------------|----------------|---------| | --skel-base | config.baseColor | var(--skeleton-base, #e0e0e0) | | --skel-highlight | config.highlightColor | var(--skeleton-highlight, #f5f5f5) | | --skel-radius | config.borderRadius | 4px |

CSS Classes:

| Class | Role | |-------|------| | .skel-block | Base block style (display: block, background-color: var(--skel-base), border-radius: var(--skel-radius)) | | .skel-shimmer | Applied to animated blocks. In shimmer mode: gradient + animation. In pulse/none: static background. | | .skel-pulse | Applied for pulse mode animation. |

Keyframes:

| Mode | Keyframe | Duration formula | |------|----------|-----------------| | shimmer | skel-shimmer (background-position 0→200%) | 2 / speed seconds | | pulse | skel-pulse (opacity 1→0.5→1) | 1.5 / speed seconds | | none | No keyframes emitted | — |

Reduced Motion:

@media (prefers-reduced-motion: reduce) {
  .skel-shimmer, .skel-pulse {
    animation: none !important;
    opacity: 0.7 !important;
  }
}

Opt-out visibility reset:

.skel-content[data-loading="true"] [data-no-skeleton],
.skel-content[data-loading="true"] [data-skeleton-ignore] {
  position: relative;
  z-index: 11;
  visibility: visible !important;
  opacity: 1 !important;
  pointer-events: auto !important;
}

Configuration — SkeletonConfig

All visual and behavioral options are controlled through a single SkeletonConfig object. The full default is exported as DEFAULT_CONFIG.

Full Config Reference

| Option | Type | Default | Description | |--------|------|---------|-------------| | animation | "shimmer" \| "pulse" \| "none" | "shimmer" | Animation style applied to skeleton blocks. | | baseColor | string | "var(--skeleton-base, #e0e0e0)" | CSS color for the skeleton block background. Supports CSS variables. | | highlightColor | string | "var(--skeleton-highlight, #f5f5f5)" | Shimmer highlight color. Used only when animation is "shimmer". | | borderRadius | number | 4 | Pixel border radius for all non-avatar skeleton blocks. | | speed | number | 1 | Animation speed multiplier. 2 = twice as fast. Minimum 0.1 (clamped). | | minTextHeight | number | 12 | Minimum height in px for each text line bar. | | maxDepth | number | 12 | Maximum DOM traversal depth. Increase for deeply nested UIs. | | lastLineRatio | number | 0.7 | Width ratio (0–1) for the last line in multi-line text blocks. | | iconMaxSize | number | 32 | <svg> elements ≤ this size (px) are classified as icon. | | minImageArea | number | 900 | Minimum area in px² for a non-<img> element to be classified as image. | | transitionDuration | number | 300 | Duration in ms of skeleton fade-out when loading ends. | | tableCellInsetX | number | 8 | Horizontal padding in px for skeleton bars inside table cells. | | tableCellBarHeightRatio | number | 0.45 | Height of the table-cell bar as a ratio of the cell height. | | tableCellBarMinHeight | number | 6 | Minimum height in px for table-cell skeleton bars. | | tableCellDefaultWidthRatio | number | 0.7 | Fallback width ratio (0–1) for table-cell bars when text width cannot be inferred. |

DEFAULT_CONFIG

import { DEFAULT_CONFIG } from "@ghostframes/runtime";

// DEFAULT_CONFIG:
{
  animation: "shimmer",
  baseColor: "var(--skeleton-base, #e0e0e0)",
  highlightColor: "var(--skeleton-highlight, #f5f5f5)",
  borderRadius: 4,
  speed: 1,
  minTextHeight: 12,
  maxDepth: 12,
  lastLineRatio: 0.7,
  iconMaxSize: 32,
  minImageArea: 900,
  transitionDuration: 300,
  tableCellInsetX: 8,
  tableCellBarHeightRatio: 0.45,
  tableCellBarMinHeight: 6,
  tableCellDefaultWidthRatio: 0.7,
}

Type Reference

All types are exported from the package entry point.

SkeletonRole

type SkeletonRole =
  | "text"    // <p>, heading, inline text
  | "image"   // <img>, background-image, <picture>
  | "avatar"  // Circular image (border-radius >= 50%)
  | "icon"    // <svg>, small square
  | "button"  // <button>, role="button"
  | "input"   // <input>, <textarea>, <select>
  | "video"   // <video>
  | "canvas"  // <canvas>
  | "badge"   // Small pill text (tag, chip)
  | "custom"  // User-provided slot via data-skeleton-slot
  | "skip";   // Too small or explicitly excluded

AnimationMode

type AnimationMode = "pulse" | "shimmer" | "none";

TextMeta

Populated on BlueprintNode when role === "text" or role === "table-cell".

type TextMeta = {
  lines: number;             // Estimated number of text lines
  lineHeight: number;        // px — computed from CSS lineHeight
  lastLineWidthRatio: number; // 0–1, how wide the last line is (default 0.7)
};

LayoutProps

Collected from containers for flow-mode rendering:

type LayoutProps = {
  display: string;
  flexDirection: string;
  flexWrap: string;
  justifyContent: string;
  alignItems: string;
  gap: string;
  rowGap: string;
  columnGap: string;
  gridTemplateColumns: string;
  gridTemplateRows: string;
  padding: string;
  margin: string;
  // Flex child props
  flex: string;
  flexGrow: string;
  flexShrink: string;
  flexBasis: string;
  alignSelf: string;
  // Grid child props
  gridColumn: string;
  gridRow: string;
  direction: string;
};

GhostframesPropsBase

The base props type for framework adapters (e.g. @ghostframes/runtime extends this):

type GhostframesPropsBase = {
  loading: boolean;
  config?: Partial<SkeletonConfig>;
  blueprint?: Blueprint;          // SSR pre-computed blueprint
  remeasureOnResize?: boolean;
};

Data Attributes

Control skeleton behavior from markup — no code changes required.

| Attribute | Scope | Effect | |-----------|-------|--------| | data-skeleton-ignore | Any element | Exclude this element and all its descendants from measurement. | | data-no-skeleton | Any element | Keep element visible and interactive while the skeleton overlay is active. Uses z-index and visibility overrides. | | data-skeleton-role="<role>" | Any element | Force a specific SkeletonRole. Validated — invalid values fall through to scoring. | | data-skeleton-slot="<key>" | Any element | Mark as a custom slot. Only meaningful to framework adapters that accept slots maps. | | data-skeleton-w="<number>" | Static blueprints | Override the width (px) used in static blueprint generation. | | data-skeleton-h="<number>" | Static blueprints | Override the height (px) used in static blueprint generation. |

Static blueprint data attribute example

// Forces avatar dimensions to 64x64 in static blueprints
<img
  src={user.avatar}
  data-skeleton-role="avatar"
  data-skeleton-w={64}
  data-skeleton-h={64}
/>

Dynamic exclusion example

<!-- Measured, but always visible on top of the skeleton overlay -->
<nav data-no-skeleton>...</nav>

<!-- Completely excluded from skeleton measurement -->
<aside data-skeleton-ignore>...</aside>

Skeleton Roles

| Role | Visual | Inferred when... | |------|--------|--------------------| | text | Multi-line horizontal bars | <p>, <h1><h6>, <span>, <li>, <a>, or any leaf with text | | image | Solid rectangle | <img>, <picture>, elements with background-image, or large empty non-child elements | | avatar | Circular block | <img> or element with border-radius >= 50% and width 24–128px | | icon | Small square | <svg> smaller than iconMaxSize, or small square elements (8–32px) | | button | Pill-shaped bar | <button>, role="button", or compact <a> | | input | Rounded rectangle | <input>, <textarea>, <select>, or role="textbox"/"combobox" | | video | Aspect-ratio rectangle | <video> | | canvas | Rectangle | <canvas> | | badge | Small pill | Small element with short text, high border-radius (≥ 20%), height ≤ 28px, width ≤ 120px | | custom | Calls slot renderer | Element with data-skeleton-slot attribute | | skip | Nothing rendered | Invisible, zero-size, or explicitly data-skeleton-ignore elements |


Versioning and Changelog

This package uses Changesets for automated versioning and changelog generation.

# Add a changeset describing your change
pnpm changeset

# Apply changesets and bump versions
pnpm changeset version

# Publish to npm
pnpm changeset publish

See CHANGELOG.md for the full release history.


License

MIT © Ghostframes Contributors