Each leaf element here gets its own shimmer block.
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.
- Host: GitHub
- URL: https://github.com/aejkatappaja/phantom-ui
- Owner: Aejkatappaja
- License: mit
- Created: 2026-03-30T15:23:54.000Z (8 days ago)
- Default Branch: main
- Last Pushed: 2026-04-06T22:28:13.000Z (about 19 hours ago)
- Last Synced: 2026-04-06T23:24:44.001Z (about 18 hours ago)
- Topics: angular, custom-element, lit, loading, placeholder, qwik, react, shimmer, skeleton, solidjs, typescript, vue, webcomponent
- Language: TypeScript
- Homepage: https://aejkatappaja.github.io/phantom-ui/
- Size: 9.32 MB
- Stars: 5
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
Structure-aware skeleton loader. One Web Component. Every framework.
---
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;
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
ACME
$48.2k
2,847 users
42ms p99
```
## 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