npm package: @lukas_holdings/castdom
Description: Pixel-perfect skeleton loading screens, extracted from your real DOM. Zero-config, SSR-first, CSS-only runtime.
Author: Lukas Holdings
Latest version: 1.0.3

<p align="center">
  <a href="https://www.npmjs.com/package/@lukas_holdings/castdom"><img src="https://img.shields.io/npm/v/@lukas_holdings/castdom?style=flat-square&color=000" alt="npm version" /></a>
  <a href="https://www.npmjs.com/package/@lukas_holdings/castdom"><img src="https://img.shields.io/npm/dm/@lukas_holdings/castdom?style=flat-square&color=000" alt="npm downloads" /></a>
  <img src="https://img.shields.io/badge/license-MIT-000?style=flat-square" alt="license" />
  <img src="https://img.shields.io/badge/runtime-CSS%20only-000?style=flat-square" alt="CSS only runtime" />
  <img src="https://img.shields.io/badge/bundle-~14KB-000?style=flat-square" alt="bundle size" />
  <img src="https://img.shields.io/badge/AI--friendly-CASTDOM.md-blue?style=flat-square" alt="AI friendly" />
</p>

<h1 align="center">CastDOM</h1>

<p align="center">
  <strong>Pixel-perfect skeleton loading screens, extracted from your real DOM.</strong><br/>
  No manual measurement. No hand-tuned placeholders. Zero-config, SSR-first, CSS-only runtime.
</p>

<p align="center">
  <code>npm install @lukas_holdings/castdom</code>
</p>

<p align="center">
  <a href="https://kameeleonn.github.io/CastDOM/"><strong>Live Demo</strong></a> &bull;
  <a href="#quick-start">Quick Start</a> &bull;
  <a href="#how-it-works">How It Works</a> &bull;
  <a href="#react">React</a> &bull;
  <a href="#nextjs">Next.js</a> &bull;
  <a href="#astro">Astro</a> &bull;
  <a href="#vite">Vite</a> &bull;
  <a href="#ssr--seo">SSR & SEO</a> &bull;
  <a href="#api-reference">API</a>
</p>

<p align="center">
  <sub>Ships with <a href="CASTDOM.md"><code>CASTDOM.md</code></a> — an AI context file for Claude, Gemini, GPT, Cursor, Copilot, and Windsurf.<br/>Ask any AI assistant to integrate CastDOM and it will know every API, type, and pattern.</sub>
</p>

---

## 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
- **Accessible** — `role="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

```bash
npm install @lukas_holdings/castdom
```

### 2. Mark your components

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

```html
<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

```bash
npx castdom init
```

This creates `castdom.config.json`:

```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:

```bash
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

```tsx
// 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:

```typescript
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

```tsx
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:

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

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

### `useCastDOM` Hook

```tsx
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

```tsx
// 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>
  );
}
```

```tsx
// 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

```tsx
// 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

```tsx
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:

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

---

## Astro

### Integration

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

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

### Component Usage

```astro
---
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

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

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

---

## Vite

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

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

Then import the virtual module:

```ts
import "virtual:castdom";
```

The plugin provides:
- **HMR** — hot-reload when skeleton data changes
- **Virtual module** — `virtual:castdom` auto-loads the manifest
- **Dev middleware** — `/__castdom/extract` endpoint

---

## Vanilla JS

For non-framework usage:

```js
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

```ts
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:

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

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

### Critical CSS

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

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

---

## CLI Reference

```bash
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`

```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.

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

### Pulse

Opacity fade between 100% and 40%.

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

### Wave

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

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

### None

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

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

---

## API Reference

### Core

```typescript
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

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

### SSR

```typescript
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:

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

---

## AI-Friendly

CastDOM ships [`CASTDOM.md`](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