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

@lukas_holdings/castdom

v1.0.4

Published

Pixel-perfect skeleton loading screens, extracted from your real DOM. Zero-config, SSR-first, CSS-only runtime.

Readme


Why CastDOM?

Every skeleton screen you've ever built was a lie. You eyeballed the widths, guessed the heights, and prayed the layout didn't shift. CastDOM fixes that.

It reads getBoundingClientRect() on every visible element in your component, stores the positions as a flat array of bones, and renders them as CSS-only rectangles that match your real layout exactly — at every breakpoint.

Your Component                          CastDOM Skeleton
+----------------------------------+    +----------------------------------+
|  [====]                     [O]  |    |  [####]                     [O]  |
|                                  |    |                                  |
|  Some heading text               |    |  [################]              |
|  A longer paragraph that wraps   |    |  [##########################]   |
|  across multiple lines of text   |    |  [########################]     |
|                                  |    |                                  |
|  [  Button  ]                    |    |  [##########]                    |
+----------------------------------+    +----------------------------------+
     Real DOM                                Extracted Skeleton

Key Advantages

  • Pixel-perfect — bones are extracted from your actual rendered DOM, not approximated
  • SSR-first — skeletons render server-side as real HTML. Crawlers see them. Zero CLS
  • CSS-only runtime — no JavaScript needed to display skeletons. Just CSS animations
  • Content-aware — detects text, headings, images, avatars, buttons, and inputs
  • Responsive — captures at multiple breakpoints (375px, 768px, 1280px by default)
  • Compressed — delta-encoded bone data is 60-70% smaller than raw JSON
  • Accessiblerole="status", aria-busy, aria-label, prefers-reduced-motion
  • Framework-native — adapters for Next.js, Astro, Vite, and vanilla JS
  • Zero runtime dependencies — the client bundle is pure CSS + tiny registry

Quick Start

1. Install

npm install @lukas_holdings/castdom

2. Mark your components

Add data-castdom attributes to the components you want skeletons for:

<div data-castdom="user-card">
  <img class="avatar" src="..." />
  <h2>Jane Doe</h2>
  <p>Software Engineer at Acme Corp</p>
  <button>Follow</button>
</div>

3. Configure

npx castdom init

This creates castdom.config.json:

{
  "devServer": "http://localhost:3000",
  "outDir": ".castdom",
  "breakpoints": [375, 768, 1280],
  "color": "#e0e0e0",
  "shimmerColor": "#f0f0f0",
  "animationDuration": 1500,
  "contentAware": true,
  "targets": [
    {
      "name": "user-card",
      "selector": "[data-castdom=\"user-card\"]",
      "route": "/"
    }
  ]
}

4. Extract

Start your dev server, then:

npx castdom build
CastDOM Build
  Server:      http://localhost:3000
  Breakpoints: 375, 768, 1280px
  Targets:     1
  Output:      .castdom

  Skeletons:   1
  Bones:       12
  Raw size:    2.1 KB
  Compressed:  0.8 KB (62% smaller)
  Files:       8
  Total time:  1240ms

5. Use

// Import the loader once at your app entry
import ".castdom/loader.js";

// Use anywhere
import { CastDOM } from "@lukas_holdings/castdom/react";

function UserProfile({ userId }) {
  const { data, isLoading } = useFetch(`/api/users/${userId}`);

  return (
    <CastDOM name="user-card" loading={isLoading}>
      <UserCard data={data} />
    </CastDOM>
  );
}

That's it. The skeleton matches your real layout perfectly, at every screen size.


How It Works

Extraction Pipeline

Dev Server          Playwright           Extractor           Compiler
    |                   |                    |                   |
    |   load page       |                    |                   |
    |<------------------|                    |                   |
    |   set viewport    |                    |                   |
    |<------------------|                    |                   |
    |                   |   inject script    |                   |
    |                   |------------------->|                   |
    |                   |                    |  walk DOM tree    |
    |                   |                    |  getBoundingRect  |
    |                   |                    |  detect kinds     |
    |                   |   bone data        |                   |
    |                   |<-------------------|                   |
    |                   |                    |                   |
    |                   |          compile, compress, codegen    |
    |                   |-------------------------------------->|
    |                   |                    |                   |
    |                   |                    |          .castdom/
    |                   |                    |          manifest.json
    |                   |                    |          castdom.css
    |                   |                    |          loader.js
    |                   |                    |          index.d.ts

Bone Data Model

Each element becomes a bone — a minimal rectangle descriptor:

interface Bone {
  x: number;      // X position relative to container
  y: number;      // Y position relative to container
  w: number;      // Width
  h: number;      // Height
  r: number;      // Border radius (9999 = circle)
  kind?: BoneKind; // "text" | "heading" | "image" | "avatar" | "button" | ...
}

Content-Aware Detection

CastDOM doesn't just make rectangles. It detects what each element is:

| Element | Detection | Skeleton Shape | |---------|-----------|----------------| | <h1>-<h6> | Tag name | Rounded rectangle, heading height | | <p>, text nodes | Text content check | Multiple line rectangles | | <img> | Tag name + aspect ratio | Rectangle with image proportions | | Avatar | Small + border-radius: 50% | Circle | | <button> | Tag or role="button" | Pill shape | | <input> | Tag name | Bordered rectangle | | <hr> | Tag name | Thin divider line |

Compression

Bone data is compressed using delta encoding and zigzag varint packing:

Raw JSON:     2,100 bytes
Compressed:     800 bytes  (62% smaller)
Base64:         540 bytes  (74% smaller)

The compression pipeline:

  1. Sort bones top-to-bottom, left-to-right
  2. Delta-encode X/Y positions
  3. Quantize to half-pixels
  4. Zigzag encode signed integers
  5. Variable-length integer packing

React

<CastDOM> Component

import { CastDOM } from "@lukas_holdings/castdom/react";

<CastDOM
  name="user-card"        // Registered skeleton name
  loading={isLoading}     // Show skeleton when true
  animation="shimmer"     // "shimmer" | "pulse" | "wave" | "none"
  color="#e0e0e0"         // Base bone color
  shimmerColor="#f0f0f0"  // Shimmer highlight color
  duration={1500}         // Animation duration (ms)
  className="my-skeleton" // Additional CSS class
  onSkeletonShow={() => console.log("Skeleton visible")}
  onContentShow={() => console.log("Content rendered")}
  ariaLabel="Loading user profile"
>
  <UserCard data={data} />
</CastDOM>

<CastDOMStyle> Component

Add to your <head> for critical CSS:

import { CastDOMStyle } from "@lukas_holdings/castdom/react";

function Layout({ children }) {
  return (
    <html>
      <head>
        <CastDOMStyle />
      </head>
      <body>{children}</body>
    </html>
  );
}

useCastDOM Hook

import { useCastDOM } from "@lukas_holdings/castdom/react";

function CustomSkeleton() {
  const { exists, data, breakpoint, css, html } = useCastDOM("user-card");

  if (!exists) return <FallbackSkeleton />;

  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

Next.js

App Router

// app/layout.tsx
import { initCastDOM } from "@lukas_holdings/castdom/next";
import manifest from "../.castdom/manifest.json";

initCastDOM(manifest);

export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}
// app/dashboard/page.tsx
import { CastDOM } from "@lukas_holdings/castdom/next";

export default async function Dashboard() {
  const data = await fetchDashboard();

  return (
    <CastDOM name="dashboard" loading={!data}>
      <DashboardContent data={data} />
    </CastDOM>
  );
}

Pages Router

// pages/_app.tsx
import { initCastDOM } from "@lukas_holdings/castdom/next";
import manifest from "../.castdom/manifest.json";

initCastDOM(manifest);

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

Server-Side Props

import { getSkeletonProps } from "@lukas_holdings/castdom/next";

export async function getServerSideProps() {
  const { skeletonHTML, skeletonCSS } = getSkeletonProps(["user-card"]);
  return { props: { skeletonHTML, skeletonCSS } };
}

Auto-Generated loading.tsx

After npx castdom build, a ready-to-use loading.tsx is generated at .castdom/nextjs-loading.tsx. Copy it to your route:

cp .castdom/nextjs-loading.tsx app/dashboard/loading.tsx

Astro

Integration

// astro.config.mjs
import { defineConfig } from "astro/config";
import { castdomIntegration } from "@lukas_holdings/castdom/astro";

export default defineConfig({
  integrations: [castdomIntegration()],
});

Component Usage

---
import { skeleton } from "@lukas_holdings/castdom/astro";
const { html, css } = skeleton("user-card");
---

<style set:html={css}></style>
<div data-castdom="user-card">
  <Fragment set:html={html} />
  <slot />
</div>

View Transitions

---
import { skeleton, viewTransitionProps } from "@lukas_holdings/castdom/astro";
const { html } = skeleton("sidebar");
const transitionProps = viewTransitionProps("sidebar");
---

<div {...transitionProps}>
  <Fragment set:html={html} />
</div>

Vite

// vite.config.ts
import { defineConfig } from "vite";
import { castdom } from "@lukas_holdings/castdom/vite";

export default defineConfig({
  plugins: [castdom()],
});

Then import the virtual module:

import "virtual:castdom";

The plugin provides:

  • HMR — hot-reload when skeleton data changes
  • Virtual modulevirtual:castdom auto-loads the manifest
  • Dev middleware/__castdom/extract endpoint

Vanilla JS

For non-framework usage:

import { createCastDOM } from "@lukas_holdings/castdom";
import manifest from ".castdom/manifest.json";

const castdom = createCastDOM();
castdom.loadManifest(manifest);

// Show skeleton
const container = document.getElementById("user-card");
castdom.show("user-card", container);

// Later, hide and show real content
castdom.hide("user-card", container);

SSR & SEO

CastDOM is SSR-first. Skeletons render as server-side HTML with zero JavaScript.

Why This Matters for SEO

| Metric | Without CastDOM | With CastDOM | |--------|-----------------|--------------| | CLS | Layout shifts when content loads | Zero — skeletons match exact dimensions | | FCP | Blank until JS loads | Instant — CSS-only skeletons | | LCP | Delayed by data fetching | Skeleton paints immediately | | Accessibility | No loading indication | role="status", aria-busy, screen reader labels | | Crawler visibility | Empty containers | Structured placeholders with ARIA |

Server-Side Rendering

import { renderSkeleton, renderSSRFragment } from "@lukas_holdings/castdom/ssr";

// Single skeleton
const html = renderSkeleton(skeletonData);
// Includes: <style> + skeleton HTML + hydration data attributes

// Multiple skeletons (shared CSS)
const { head, body } = renderSSRFragment([skeleton1, skeleton2]);
// head:  <style> + <script> hydration
// body:  { "user-card": "...", "feed-item": "..." }

Hydration

SSR skeletons include data-castdom-ssr attributes. A tiny (~200 byte) inline hydration script automatically removes them once real content renders:

import { renderHydrationScript } from "@lukas_holdings/castdom/ssr";

const script = renderHydrationScript();
// <script data-castdom="hydration">(...)</script>

Critical CSS

import { renderCriticalStyleTag } from "@lukas_holdings/castdom/ssr";

const styleTag = renderCriticalStyleTag([skeleton1, skeleton2]);
// <style data-castdom="critical">@keyframes castdom-shimmer{...}...</style>

CLI Reference

castdom build [options]    # Extract skeletons and generate files
castdom init               # Create castdom.config.json template
castdom list               # List skeletons in the manifest
castdom clean              # Remove generated output directory

Build Options

| Flag | Default | Description | |------|---------|-------------| | --url <url> | http://localhost:3000 | Dev server URL | | --config <path> | castdom.config.json | Config file path | | --out <dir> | .castdom | Output directory | | --breakpoints <list> | 375,768,1280 | Comma-separated breakpoints | | --no-headless | false | Show browser UI (for debugging) | | --verbose | false | Detailed progress output |


Configuration

castdom.config.json

{
  "devServer": "http://localhost:3000",
  "outDir": ".castdom",
  "breakpoints": [375, 768, 1280],
  "color": "#e0e0e0",
  "shimmerColor": "#f0f0f0",
  "animationDuration": 1500,
  "contentAware": true,
  "minBoneSize": 4,
  "targets": [
    {
      "name": "user-card",
      "selector": "[data-castdom=\"user-card\"]",
      "route": "/"
    },
    {
      "name": "feed-item",
      "selector": "[data-castdom=\"feed-item\"]",
      "route": "/feed"
    }
  ]
}

| Option | Type | Default | Description | |--------|------|---------|-------------| | devServer | string | http://localhost:3000 | URL of your dev server | | outDir | string | .castdom | Where to write generated files | | breakpoints | number[] | [375, 768, 1280] | Viewport widths to capture | | color | string | #e0e0e0 | Base bone color | | shimmerColor | string | #f0f0f0 | Shimmer highlight color | | animationDuration | number | 1500 | Animation cycle duration (ms) | | contentAware | boolean | true | Detect element types for shaped bones | | minBoneSize | number | 4 | Skip elements smaller than this (px) |

Targets

Each target defines a component to extract:

| Field | Required | Description | |-------|----------|-------------| | name | Yes | Unique skeleton identifier | | selector | Yes | CSS selector for the container element | | route | No | Page route to navigate to before extracting |


Animation Types

CastDOM supports four animation types, all CSS-only:

Shimmer (default)

A gradient sweep from left to right. Classic skeleton animation.

<CastDOM name="card" animation="shimmer" />

Pulse

Opacity fade between 100% and 40%.

<CastDOM name="card" animation="pulse" />

Wave

Staggered opacity animation — each bone animates with a 50ms delay.

<CastDOM name="card" animation="wave" />

None

Static gray rectangles. Also used automatically when prefers-reduced-motion: reduce is active.

<CastDOM name="card" animation="none" />

API Reference

Core

import {
  loadManifest,     // Register all skeletons from generated manifest
  register,         // Register a single skeleton
  get,              // Get skeleton entry by name
  has,              // Check if skeleton exists
  names,            // List all registered skeleton names
  configure,        // Set global config (colors, animation, etc.)
  getAllCSS,        // Get combined CSS for all registered skeletons
  extractBones,     // Extract bones from a DOM element (browser-side)
  compressBones,    // Compress bone data for storage
  decompressBones,  // Decompress bone data
} from "@lukas_holdings/castdom";

React

import {
  CastDOM,          // Wrapper component
  CastDOMStyle,     // Critical CSS component for <head>
  useCastDOM,       // Hook for programmatic access
} from "@lukas_holdings/castdom/react";

SSR

import {
  renderSkeleton,          // Render skeleton to HTML string
  renderSkeletons,         // Render multiple with shared CSS
  renderCriticalStyleTag,  // Generate <style> tag
  renderHydrationScript,   // Generate hydration <script>
  renderSSRFragment,       // Complete head + body fragments
} from "@lukas_holdings/castdom/ssr";

Generated Output

After running npx castdom build, the .castdom/ directory contains:

.castdom/
  manifest.json          # All skeleton data (register with loadManifest)
  castdom.css            # Critical CSS for all skeletons
  index.js               # ESM module with tree-shakeable exports
  index.d.ts             # TypeScript declarations
  loader.js              # Auto-registers all skeletons on import
  nextjs-loading.tsx     # Ready-to-use Next.js loading component
  skeletons/
    user-card.json       # Individual skeleton data
    user-card.js         # Individual ESM export
    feed-item.json
    feed-item.js

TypeScript

CastDOM is written in TypeScript and ships full type declarations. After extraction, skeleton names are typed:

// .castdom/index.d.ts (auto-generated)
export type SkeletonName = "user-card" | "feed-item";

AI-Friendly

CastDOM ships CASTDOM.md — a comprehensive context file for AI coding assistants (Claude, Gemini, GPT, Cursor, Copilot, Windsurf, etc.). It contains every type, API, integration pattern, and CLI command so any AI can help you integrate CastDOM correctly.


Browser Support

CastDOM's runtime is pure CSS — it works everywhere CSS animations work:

  • Chrome 60+
  • Firefox 60+
  • Safari 12+
  • Edge 79+

The extraction tool (CLI) requires Node.js 18+ and Playwright.


License

MIT