# phantom-ui: a skeleton loader that measures your real DOM

> phantom-ui is a Lit based Web Component that generates shimmer placeholders by measuring the elements you already rendered, so a loading state stops being a second component to maintain. It works in any framework, ships in about eight kilobytes minzipped, and its own documentation is unusually explicit about how the measurement works and where it does not apply.

**Aejkatappaja/phantom-ui** — Structure-aware skeleton loader. One Web Component, every framework.

- Repository: https://github.com/Aejkatappaja/phantom-ui
- Website: https://aejkatappaja.github.io/phantom-ui/demo
- Stars: 808 · Forks: 31
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/aejkatappaja-phantom-ui

## The loading state comes from the component you already wrote

phantom-ui is described as a structure-aware skeleton loader, one Web Component and every framework. The pitch in the project page is blunt: stop building skeleton screens by hand, wrap your real UI in a phantom-ui element and it generates shimmer placeholders automatically by measuring your actual DOM at runtime.

The mechanism is the interesting part. It renders your real component with invisible text, measures the position and size of every leaf element with getBoundingClientRect, and overlays animated shimmer blocks at the exact same coordinates. Container backgrounds and borders stay visible, which gives a natural card outline while the data is loading.

The summary of the approach is the line to remember: the real component is the skeleton template. There is no separate skeleton component to write.

That solves a problem most teams hit without noticing. The project puts it as a matter of drift: traditional skeleton loaders require you to build and maintain a second version of every component, just for the loading state, and when the real component changes, the skeleton drifts out of sync. A card that gains a badge in the real version keeps its old shape in the skeleton version, and nobody notices until a user sees the wrong outline during a slow request.

The alternative is worth naming, because it is what most teams do by default: write a skeleton component per component and keep the two in step manually. phantom-ui replaces that manual step with a measurement, and the price is that the placeholder can only describe shapes that exist in the real DOM.

## One custom element, no framework adapters

Because phantom-ui is a standard Web Component built with Lit, it works in React, Vue, Svelte, Angular, Solid, Qwik, HTMX, or plain HTML, with no framework adapters needed.

That is the reason for choosing a custom element rather than a React or Vue component. A component library that ships per-framework versions has to be released, versioned and debugged once per framework, and it inherits each framework's reactivity and rendering rules. A custom element is an object the browser already knows how to instantiate, so the same artefact runs in every host, and the frameworks in question treat it as an opaque element with attributes and children.

The topic list on the repository is a good proxy for the audience it has been tested against: Angular, HTMX, Qwik, React, Solid, Svelte, Vue, Lit and web components, and the examples directory in the repository contains separate applications for Angular, HTMX, Qwik, React, React Query, Solid, Stencil and Vue.

The cost of the approach is that the integration is not entirely free in every framework. React, Solid and Qwik need a JSX type declaration so that the custom element is recognised in JSX, and server rendered frameworks need a stylesheet import to avoid a flash of unstyled content before hydration. The project handles both with an optional setup command, and both are documented in detail with manual instructions for people who would rather not run it.

The package is published on npm as @aejkatappaja/phantom-ui, is listed on webcomponents.org, and the licence is MIT.

## Four ways to install it, from a registry to two copied files

The install section is organised by how much control you want over the artefact, and it covers four levels.

The first is the normal package manager route, one line whichever tool you use:

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

The second needs no build step at all, a script tag pointing at the CDN build:

```bash
<script src="https://cdn.jsdelivr.net/npm/@aejkatappaja/phantom-ui/dist/phantom-ui.cdn.js"></script>
```

The third is for people who cannot install from a registry but prefer a single module. There is a standalone ES module with Lit bundled in, one file of 34 kB minified and 11.5 kB gzipped with no runtime dependency, imported as:

```ts
import "@aejkatappaja/phantom-ui/standalone";
```

The fourth is for people who cannot reach a registry at all. Two files are copied into the project and imported by path, the component and its types:

```ts
import "./phantom-ui.standalone.js";
import type { PhantomUi } from "./phantom-ui.standalone.js";
```

The instructions are specific about which two to take and why. The package ships an unminified build so a review can read it, a minified one, and a declaration file with no imports of any kind. The guidance is to take the unminified file and the types, because TypeScript pairs a declaration to a module by exact filename, so those two names go together and the types apply with nothing to configure.

The claim that follows is worth noting: the element registers itself on import, and the declarations reference nothing external, so a project with no node_modules at all still type-checks under strict with skipLibCheck set to false.

One warning is attached to all of this. The library is MIT, so vendoring is fine, but you should note the version you copied somewhere near the file, since a vendored copy does not get updates.

## The init command and the deliberate absence of a postinstall hook

The component works with zero setup, and the optional command exists for two specific conveniences.

The command is run once from your project root, and the tool is the package's own binary entry:

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

It detects your project setup and handles two things. The first is JSX type declarations: for React, Solid and Qwik it generates a phantom-ui.d.ts file in your src directory so that the phantom-ui element is recognised in JSX, while Vue, Svelte and Angular work out of the box without any type declaration.

The second is SSR pre-hydration CSS: for Next.js, Nuxt, SvelteKit, Remix and Qwik it adds an import of the package's ssr.css to your layout file, which prevents content flash before hydration.

What the command does not do is run behind your back. The library deliberately has no postinstall hook, so your source is never modified unless you ask for it. For a library that edits files in your repository, that is the right default, and it is stated plainly rather than buried.

If you would rather not run the command at all, the documentation includes both steps manually, including the module augmentation blocks for React, Solid and Qwik, and a table of which layout file to edit for each framework: app/layout.tsx for the Next.js App Router, pages/_app.tsx for the Next.js Pages Router, app.vue for Nuxt, src/routes/+layout.svelte for SvelteKit, app/root.tsx for Remix and src/root.tsx for Qwik.

## The measurement approach has edges worth knowing

The mechanism is elegant, and the edges come from the same place the elegance does.

The component works by measuring leaf elements at runtime. That means the quality of the placeholder depends on what your component actually renders when it is in the loading state, and on the browser being able to measure those elements at that moment. An element with no dimensions, an image that has not loaded, or a list that is empty renders a placeholder that describes nothing.

It also means the measurement happens on the client. The project handles the server rendered case with the ssr.css import, which prevents content flash before hydration, but the shimmer itself is a client side effect.

There is a second, subtler constraint that the documentation calls out in its type declarations section. The declared class extends HTMLElement rather than LitElement, which is a narrower view of the same object, so updateComplete is declared explicitly and the rest of the public API is unchanged. In other words, the public types are not the Lit types with the internal machinery exposed, they are a deliberately smaller surface. If you were expecting the full Lit element type, you will not get it.

A third point is about the build outputs rather than the runtime. Inside the npm package the standalone builds are phantom-ui.standalone.js and phantom-ui.standalone.dev.js, resolved through the exports map rather than by filename, and both are bundled output rather than the original TypeScript, because the sources import Lit and therefore cannot be compiled on their own.

None of that makes the tool wrong. It makes it a browser library with browser limitations, and the trade is a single component that cannot drift against a placeholder that is only as good as the DOM it measures.

## Bundle size, tooling and a young release line

The package page advertises a minzipped size of about 8 kB, and the standalone build is 34 kB minified and 11.5 kB gzipped with Lit bundled in, so the numbers are consistent with a small dependency.

The build pipeline in the package manifest shows how that is achieved. The ESM build is an esbuild bundle with the format set to esm, minified, targeting es2022, with packages marked external so Lit is not duplicated in your application bundle. The CDN build is the same source in an iife format. The standalone build bundles everything including Lit. The SSR stylesheet is generated by a script run with Bun, then formatted with biome and copied into the distribution folder, and the build ends with a custom elements manifest analysis step, which is how the webcomponents.org listing and the custom-elements.json file stay in step with the code.

The repository is a modern TypeScript project with a Bun lockfile, Biome for formatting and linting with its own plugins, a Storybook directory for component development, a playground directory, a web test runner configuration for browser tests, and a set of example applications including one for React Query and one for Stencil.

On the release side, the versions are recent and quick. v1.6.0 and v1.6.1 were both published on 2026-07-31, with v1.5.0 the day before, and the last push was on 2026-07-31. The 1.x numbering means the API is meant to be stable, but a project publishing three versions in two days is one that will move underneath you, which is the argument for the registry install over a vendored copy.

## Conclusion

phantom-ui suits a team whose components keep drifting between their real and their loading state, and who want one component to serve both. It is the wrong choice if your loading state needs to show something your real DOM does not contain, since the placeholder is derived from measured geometry, and it is the wrong choice if you cannot accept a runtime measurement pass over your component tree on every render of a loading state. Verify three things before you adopt it: that the elements you want outlined are leaf elements the component can measure, that your framework's server rendering path picks up the ssr.css import so the layout does not flash before hydration, and that the version you vendor is recorded, because a vendored copy does not receive updates.

## FAQ

### What is phantom-ui?

It is a structure-aware skeleton loader published as a single Web Component built with Lit. It renders your real component with invisible text, measures every leaf element with getBoundingClientRect and overlays animated shimmer blocks at the same coordinates, so the real component doubles as the skeleton template.

### Which frameworks does phantom-ui work with?

It is a standard Web Component, so it works in React, Vue, Svelte, Angular, Solid, Qwik, HTMX and plain HTML without framework adapters. React, Solid and Qwik need a JSX type declaration, which the init command generates, while Vue, Svelte and Angular work out of the box.

### How do I install phantom-ui?

With your package manager, for example bun add or npm install of @aejkatappaja/phantom-ui, with a script tag pointing at the CDN build for a no build setup, or with the standalone ES module that has Lit bundled in at 34 kB minified and 11.5 kB gzipped with no runtime dependency.

### What does the phantom-ui init command do?

It detects your project setup, generates a phantom-ui.d.ts in your src directory for React, Solid and Qwik, and adds an import of the package's ssr.css to your layout file for Next.js, Nuxt, SvelteKit, Remix and Qwik to prevent a flash before hydration. The library deliberately has no postinstall hook, so nothing modifies your source unless you run it.

### Can I vendor phantom-ui into my repository?

Yes, the library is MIT licensed. Copy the unminified standalone build and the type declarations into the project and import them by path, taking the unminified file because TypeScript pairs a declaration to a module by exact filename. Note the version you copied, since a vendored copy does not get updates.

### How large is phantom-ui?

The npm badge reports a minzipped size of about 8 kB, and the standalone build with Lit bundled in is 34 kB minified and 11.5 kB gzipped. The main ESM build keeps packages external so Lit is not duplicated in an application bundle.

## Sources

- [Aejkatappaja/phantom-ui on GitHub](https://github.com/Aejkatappaja/phantom-ui)
- [License: MIT](https://github.com/Aejkatappaja/phantom-ui/blob/main/LICENSE)
- [Project website](https://aejkatappaja.github.io/phantom-ui/demo)
- [README](https://github.com/Aejkatappaja/phantom-ui/blob/main/README.md)
- [Releases](https://github.com/Aejkatappaja/phantom-ui/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/aejkatappaja-phantom-ui
