An open API service indexing awesome lists of open source software.

https://github.com/aejkatappaja/phantom-ui

Structure-aware skeleton loader. One Web Component, every framework.
https://github.com/aejkatappaja/phantom-ui

angular custom-element lit loading placeholder qwik react shimmer skeleton solidjs typescript vue webcomponent

Last synced: about 8 hours ago
JSON representation

Structure-aware skeleton loader. One Web Component, every framework.

Awesome Lists containing this project

README

          


phantom-ui


Structure-aware skeleton loader. One Web Component. Every framework.


npm version
bundle size
license
Published on webcomponents.org


Documentation ·
Live Demo

---




phantom-ui demo


Stop building skeleton screens by hand. Wrap your real UI in `` and it generates shimmer placeholders automatically by measuring your actual DOM at runtime.

No separate skeleton components to maintain. No copy-pasting layouts. The real component _is_ the skeleton template.

## Why

Traditional skeleton loaders require you to build and maintain a second version of every component, just for the loading state. When the real component changes, the skeleton drifts out of sync.

`phantom-ui` takes a different approach. It renders your real component with invisible text, measures the position and size of every leaf element (`getBoundingClientRect`), and overlays animated shimmer blocks at the exact same coordinates. Container backgrounds and borders stay visible, giving a natural card outline while loading.

Because it is a standard Web Component (built with Lit), it works in React, Vue, Svelte, Angular, Solid, Qwik, or plain HTML. No framework adapters needed.

## Install

```bash
bun add @aejkatappaja/phantom-ui # bun
npm install @aejkatappaja/phantom-ui # npm
pnpm add @aejkatappaja/phantom-ui # pnpm
yarn add @aejkatappaja/phantom-ui # yarn
```

Or drop in a script tag with no build step:

```html

```

## Quick start

```html



Ada Lovelace


First computer programmer, probably.


```

Set `loading` to show the shimmer. Remove it to reveal the real content.

## Framework examples

### React

```tsx
import "@aejkatappaja/phantom-ui";

function ProfileCard({ user, isLoading }: Props) {
return (



{user?.name ?? "Placeholder Name"}


{user?.bio ?? "A few words about this person go here."}




);
}
```

### Vue

```vue

import "@aejkatappaja/phantom-ui";

const props = defineProps<{ loading: boolean }>();




Ada Lovelace


First computer programmer, probably.



```

### Svelte

```svelte

import "@aejkatappaja/phantom-ui";

export let loading = true;


avatar

Ada Lovelace


First computer programmer, probably.


```

### Angular

```typescript
import { Component, signal, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "@aejkatappaja/phantom-ui";

@Component({
selector: "app-profile",
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `



Ada Lovelace


First computer programmer, probably.




`,
})
export class ProfileComponent {
loading = signal(true);
}
```

### Solid

```tsx
import { createSignal } from "solid-js";
import "@aejkatappaja/phantom-ui";

function ProfileCard() {
const [loading, setLoading] = createSignal(true);

return (



Ada Lovelace


First computer programmer, probably.




);
}
```

### SSR frameworks (Next.js, Nuxt, SvelteKit, Remix)

The component needs browser APIs to measure the DOM. Import it client-side only:

```tsx
// Next.js
"use client";
import { useEffect } from "react";

export default function Page() {
useEffect(() => { import("@aejkatappaja/phantom-ui"); }, []);
return ...;
}
```

```vue

onMounted(() => import("@aejkatappaja/phantom-ui"));


...

```

```svelte

import { onMount } from "svelte";
onMount(() => import("@aejkatappaja/phantom-ui"));

```

The `` tag can exist in server-rendered HTML. The browser treats it as an unknown element until hydration, then the Web Component activates and measures the DOM. Content renders normally on the server, which is good for SEO.

## TypeScript

The package ships full type definitions. A `postinstall` script automatically detects your framework and generates a `phantom-ui.d.ts` in your `src/` directory. No extra step needed.

Vue, Svelte, and Angular work out of the box without any type declaration.

If the postinstall did not run (CI, monorepos, `--ignore-scripts`), you can generate it manually:

```bash
npx @aejkatappaja/phantom-ui init # npm
bunx @aejkatappaja/phantom-ui init # bun
pnpx @aejkatappaja/phantom-ui init # pnpm
yarn dlx @aejkatappaja/phantom-ui init # yarn
```

Or create the file yourself:

**React**

```typescript
import type { PhantomUiAttributes } from "@aejkatappaja/phantom-ui";

declare module "react/jsx-runtime" {
export namespace JSX {
interface IntrinsicElements {
"phantom-ui": PhantomUiAttributes;
}
}
}
```

**Solid**

```typescript
import type { SolidPhantomUiAttributes } from "@aejkatappaja/phantom-ui";

declare module "solid-js" {
namespace JSX {
interface IntrinsicElements {
"phantom-ui": SolidPhantomUiAttributes;
}
}
}
```

**Qwik**

```typescript
import type { PhantomUiAttributes } from "@aejkatappaja/phantom-ui";

declare module "@builder.io/qwik" {
namespace QwikJSX {
interface IntrinsicElements {
"phantom-ui": PhantomUiAttributes & Record;
}
}
}
```

## Attributes

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | `false` | Show shimmer overlay or real content |
| `shimmer-color` | `string` | `rgba(255,255,255,0.3)` | Color of the animated gradient sweep |
| `background-color` | `string` | `rgba(255,255,255,0.08)` | Background of each shimmer block |
| `duration` | `number` | `1.5` | Animation cycle in seconds |
| `fallback-radius` | `number` | `4` | Border radius (px) for flat elements like text |

## Fine-grained control

Two data attributes let you control which elements get shimmer treatment:

**`data-shimmer-ignore`** keeps an element and all its descendants visible during loading. Useful for logos, brand marks, or live indicators that should always be shown.

**`data-shimmer-no-children`** captures the element as one single shimmer block instead of recursing into its children. Useful for dense metric groups that should appear as a single placeholder.

```html




$48.2k
2,847 users
42ms p99


Each leaf element here gets its own shimmer block.



```

## How it works

1. Your real content is rendered in the DOM with `color: transparent` and media elements hidden. Container backgrounds and borders stay visible, preserving the natural card/section outline.

2. The component walks the DOM tree and identifies "leaf" elements: text nodes, images, buttons, inputs, and anything without child elements. Container divs are recursed into, not captured.

3. Each leaf element is measured with `getBoundingClientRect()` relative to the host. Border radius is read from `getComputedStyle()`. Table cells get special handling to measure actual text width, not cell width.

4. An absolutely-positioned overlay renders one shimmer block per measured element, with a CSS gradient animation sweeping across each block.

5. A `ResizeObserver` and `MutationObserver` re-measure automatically when the layout changes (window resize, content injection, DOM mutations).

6. When `loading` is removed, the overlay is destroyed and real content is revealed.

## CSS custom properties

You can style the component from the outside using CSS custom properties instead of (or in addition to) attributes:

```css
phantom-ui {
--shimmer-color: rgba(100, 200, 255, 0.3);
--shimmer-duration: 2s;
--shimmer-bg: rgba(100, 200, 255, 0.08);
}
```

## Custom Elements Manifest

The package ships a `custom-elements.json` manifest, which gives IDE autocomplete, Storybook autodocs, and framework tooling the full picture of attributes, properties, slots, and types.

## Bundle size

The CDN build (Lit included) is ~22kb / ~8kb gzipped.

When used as an ES module with a bundler, Lit is likely already in your dependency tree, bringing the component cost down to under 2kb.

## Development

```bash
bun install
bun run storybook # dev server on :6006
bun run build # tsc + custom elements manifest + CDN bundle
bun run lint # biome check
bun run lint:fix # biome auto-fix
```

The `examples/` directory contains test apps for React, Vue, Solid, Angular, and Qwik, each wired to the local package.

## Acknowledgements

The structure-aware approach is inspired by [shimmer-from-structure](https://github.com/darula-hpp/shimmer-from-structure), which pioneered the idea of measuring real DOM to generate skeleton placeholders. phantom-ui reimagines this concept as a single universal Web Component instead of framework-specific adapters.

## License

MIT