{"id":48483243,"url":"https://github.com/aejkatappaja/phantom-ui","last_synced_at":"2026-04-07T09:01:08.083Z","repository":{"id":348058580,"uuid":"1196315863","full_name":"Aejkatappaja/phantom-ui","owner":"Aejkatappaja","description":"Structure-aware skeleton loader. One Web Component, every framework.","archived":false,"fork":false,"pushed_at":"2026-04-06T22:28:13.000Z","size":9768,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-06T23:24:44.001Z","etag":null,"topics":["angular","custom-element","lit","loading","placeholder","qwik","react","shimmer","skeleton","solidjs","typescript","vue","webcomponent"],"latest_commit_sha":null,"homepage":"https://aejkatappaja.github.io/phantom-ui/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Aejkatappaja.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-30T15:23:54.000Z","updated_at":"2026-04-06T23:19:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Aejkatappaja/phantom-ui","commit_stats":null,"previous_names":["aejkatappaja/phantom-ui"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Aejkatappaja/phantom-ui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Aejkatappaja%2Fphantom-ui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Aejkatappaja%2Fphantom-ui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Aejkatappaja%2Fphantom-ui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Aejkatappaja%2Fphantom-ui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Aejkatappaja","download_url":"https://codeload.github.com/Aejkatappaja/phantom-ui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Aejkatappaja%2Fphantom-ui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31506578,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","custom-element","lit","loading","placeholder","qwik","react","shimmer","skeleton","solidjs","typescript","vue","webcomponent"],"created_at":"2026-04-07T09:01:05.245Z","updated_at":"2026-04-07T09:01:08.069Z","avatar_url":"https://github.com/Aejkatappaja.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"logo-phantom.svg\" alt=\"phantom-ui\" width=\"200\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eStructure-aware skeleton loader. One Web Component. Every framework.\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@aejkatappaja/phantom-ui\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@aejkatappaja/phantom-ui.svg?style=flat-square\" alt=\"npm version\" /\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/minzipped-~8kb-blue?style=flat-square\" alt=\"bundle size\" /\u003e\n  \u003ca href=\"https://github.com/Aejkatappaja/phantom-ui/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/npm/l/@aejkatappaja/phantom-ui?style=flat-square\" alt=\"license\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.webcomponents.org/element/@aejkatappaja/phantom-ui\"\u003e\u003cimg src=\"https://img.shields.io/badge/webcomponents.org-published-blue.svg?style=flat-square\" alt=\"Published on webcomponents.org\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://aejkatappaja.github.io/phantom-ui/\"\u003eDocumentation\u003c/a\u003e \u0026middot;\n  \u003ca href=\"https://aejkatappaja.github.io/phantom-ui/demo/\"\u003eLive Demo\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n\u003cbr /\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003cpicture\u003e\n    \u003cimg src=\"preview.gif\" alt=\"phantom-ui demo\" width=\"640\" /\u003e\n  \u003c/picture\u003e\n\u003c/div\u003e\n\n\u003cbr /\u003e\n\nStop building skeleton screens by hand. Wrap your real UI in `\u003cphantom-ui\u003e` and it generates shimmer placeholders automatically by measuring your actual DOM at runtime.\n\nNo separate skeleton components to maintain. No copy-pasting layouts. The real component _is_ the skeleton template.\n\n## Why\n\nTraditional 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.\n\n`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.\n\nBecause 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.\n\n## Install\n\n```bash\nbun add @aejkatappaja/phantom-ui     # bun\nnpm install @aejkatappaja/phantom-ui # npm\npnpm add @aejkatappaja/phantom-ui    # pnpm\nyarn add @aejkatappaja/phantom-ui    # yarn\n```\n\nOr drop in a script tag with no build step:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/@aejkatappaja/phantom-ui/dist/phantom-ui.cdn.js\"\u003e\u003c/script\u003e\n```\n\n## Quick start\n\n```html\n\u003cphantom-ui loading\u003e\n  \u003cdiv class=\"card\"\u003e\n    \u003cimg src=\"avatar.png\" width=\"48\" height=\"48\" style=\"border-radius: 50%\" /\u003e\n    \u003ch3\u003eAda Lovelace\u003c/h3\u003e\n    \u003cp\u003eFirst computer programmer, probably.\u003c/p\u003e\n  \u003c/div\u003e\n\u003c/phantom-ui\u003e\n```\n\nSet `loading` to show the shimmer. Remove it to reveal the real content.\n\n## Framework examples\n\n### React\n\n```tsx\nimport \"@aejkatappaja/phantom-ui\";\n\nfunction ProfileCard({ user, isLoading }: Props) {\n  return (\n    \u003cphantom-ui loading={isLoading || undefined}\u003e\n      \u003cdiv className=\"card\"\u003e\n        \u003cimg src={user?.avatar ?? \"/placeholder.png\"} className=\"avatar\" /\u003e\n        \u003ch3\u003e{user?.name ?? \"Placeholder Name\"}\u003c/h3\u003e\n        \u003cp\u003e{user?.bio ?? \"A few words about this person go here.\"}\u003c/p\u003e\n      \u003c/div\u003e\n    \u003c/phantom-ui\u003e\n  );\n}\n```\n\n### Vue\n\n```vue\n\u003cscript setup lang=\"ts\"\u003e\nimport \"@aejkatappaja/phantom-ui\";\n\nconst props = defineProps\u003c{ loading: boolean }\u003e();\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cphantom-ui :loading=\"props.loading\"\u003e\n    \u003cdiv class=\"card\"\u003e\n      \u003cimg src=\"/avatar.png\" class=\"avatar\" /\u003e\n      \u003ch3\u003eAda Lovelace\u003c/h3\u003e\n      \u003cp\u003eFirst computer programmer, probably.\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/phantom-ui\u003e\n\u003c/template\u003e\n```\n\n### Svelte\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n  import \"@aejkatappaja/phantom-ui\";\n\n  export let loading = true;\n\u003c/script\u003e\n\n\u003cphantom-ui {loading}\u003e\n  \u003cdiv class=\"card\"\u003e\n    \u003cimg src=\"/avatar.png\" alt=\"avatar\" class=\"avatar\" /\u003e\n    \u003ch3\u003eAda Lovelace\u003c/h3\u003e\n    \u003cp\u003eFirst computer programmer, probably.\u003c/p\u003e\n  \u003c/div\u003e\n\u003c/phantom-ui\u003e\n```\n\n### Angular\n\n```typescript\nimport { Component, signal, CUSTOM_ELEMENTS_SCHEMA } from \"@angular/core\";\nimport \"@aejkatappaja/phantom-ui\";\n\n@Component({\n  selector: \"app-profile\",\n  schemas: [CUSTOM_ELEMENTS_SCHEMA],\n  template: `\n    \u003cphantom-ui [attr.loading]=\"loading() ? '' : null\"\u003e\n      \u003cdiv class=\"card\"\u003e\n        \u003cimg src=\"/avatar.png\" class=\"avatar\" /\u003e\n        \u003ch3\u003eAda Lovelace\u003c/h3\u003e\n        \u003cp\u003eFirst computer programmer, probably.\u003c/p\u003e\n      \u003c/div\u003e\n    \u003c/phantom-ui\u003e\n  `,\n})\nexport class ProfileComponent {\n  loading = signal(true);\n}\n```\n\n### Solid\n\n```tsx\nimport { createSignal } from \"solid-js\";\nimport \"@aejkatappaja/phantom-ui\";\n\nfunction ProfileCard() {\n  const [loading, setLoading] = createSignal(true);\n\n  return (\n    \u003cphantom-ui attr:loading={loading() || undefined}\u003e\n      \u003cdiv class=\"card\"\u003e\n        \u003cimg src=\"/avatar.png\" class=\"avatar\" /\u003e\n        \u003ch3\u003eAda Lovelace\u003c/h3\u003e\n        \u003cp\u003eFirst computer programmer, probably.\u003c/p\u003e\n      \u003c/div\u003e\n    \u003c/phantom-ui\u003e\n  );\n}\n```\n\n### SSR frameworks (Next.js, Nuxt, SvelteKit, Remix)\n\nThe component needs browser APIs to measure the DOM. Import it client-side only:\n\n```tsx\n// Next.js\n\"use client\";\nimport { useEffect } from \"react\";\n\nexport default function Page() {\n  useEffect(() =\u003e { import(\"@aejkatappaja/phantom-ui\"); }, []);\n  return \u003cphantom-ui loading\u003e...\u003c/phantom-ui\u003e;\n}\n```\n\n```vue\n\u003c!-- Nuxt --\u003e\n\u003cscript setup\u003e\nonMounted(() =\u003e import(\"@aejkatappaja/phantom-ui\"));\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cClientOnly\u003e\n    \u003cphantom-ui loading\u003e...\u003c/phantom-ui\u003e\n  \u003c/ClientOnly\u003e\n\u003c/template\u003e\n```\n\n```svelte\n\u003c!-- SvelteKit --\u003e\n\u003cscript\u003e\n  import { onMount } from \"svelte\";\n  onMount(() =\u003e import(\"@aejkatappaja/phantom-ui\"));\n\u003c/script\u003e\n```\n\nThe `\u003cphantom-ui\u003e` 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.\n\n## TypeScript\n\nThe 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.\n\nVue, Svelte, and Angular work out of the box without any type declaration.\n\nIf the postinstall did not run (CI, monorepos, `--ignore-scripts`), you can generate it manually:\n\n```bash\nnpx @aejkatappaja/phantom-ui init    # npm\nbunx @aejkatappaja/phantom-ui init   # bun\npnpx @aejkatappaja/phantom-ui init   # pnpm\nyarn dlx @aejkatappaja/phantom-ui init  # yarn\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eOr create the file yourself:\u003c/summary\u003e\n\n**React**\n\n```typescript\nimport type { PhantomUiAttributes } from \"@aejkatappaja/phantom-ui\";\n\ndeclare module \"react/jsx-runtime\" {\n  export namespace JSX {\n    interface IntrinsicElements {\n      \"phantom-ui\": PhantomUiAttributes;\n    }\n  }\n}\n```\n\n**Solid**\n\n```typescript\nimport type { SolidPhantomUiAttributes } from \"@aejkatappaja/phantom-ui\";\n\ndeclare module \"solid-js\" {\n  namespace JSX {\n    interface IntrinsicElements {\n      \"phantom-ui\": SolidPhantomUiAttributes;\n    }\n  }\n}\n```\n\n**Qwik**\n\n```typescript\nimport type { PhantomUiAttributes } from \"@aejkatappaja/phantom-ui\";\n\ndeclare module \"@builder.io/qwik\" {\n  namespace QwikJSX {\n    interface IntrinsicElements {\n      \"phantom-ui\": PhantomUiAttributes \u0026 Record\u003cstring, unknown\u003e;\n    }\n  }\n}\n```\n\n\n\u003c/details\u003e\n\n## Attributes\n\n| Attribute | Type | Default | Description |\n| --- | --- | --- | --- |\n| `loading` | `boolean` | `false` | Show shimmer overlay or real content |\n| `shimmer-color` | `string` | `rgba(255,255,255,0.3)` | Color of the animated gradient sweep |\n| `background-color` | `string` | `rgba(255,255,255,0.08)` | Background of each shimmer block |\n| `duration` | `number` | `1.5` | Animation cycle in seconds |\n| `fallback-radius` | `number` | `4` | Border radius (px) for flat elements like text |\n\n## Fine-grained control\n\nTwo data attributes let you control which elements get shimmer treatment:\n\n**`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.\n\n**`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.\n\n```html\n\u003cphantom-ui loading\u003e\n  \u003cdiv class=\"dashboard\"\u003e\n    \u003cdiv class=\"logo\" data-shimmer-ignore\u003eACME\u003c/div\u003e\n    \u003cdiv class=\"kpi-row\" data-shimmer-no-children\u003e\n      \u003cspan\u003e$48.2k\u003c/span\u003e\n      \u003cspan\u003e2,847 users\u003c/span\u003e\n      \u003cspan\u003e42ms p99\u003c/span\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"content\"\u003e\n      \u003cp\u003eEach leaf element here gets its own shimmer block.\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/phantom-ui\u003e\n```\n\n## How it works\n\n1. 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.\n\n2. 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.\n\n3. 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.\n\n4. An absolutely-positioned overlay renders one shimmer block per measured element, with a CSS gradient animation sweeping across each block.\n\n5. A `ResizeObserver` and `MutationObserver` re-measure automatically when the layout changes (window resize, content injection, DOM mutations).\n\n6. When `loading` is removed, the overlay is destroyed and real content is revealed.\n\n## CSS custom properties\n\nYou can style the component from the outside using CSS custom properties instead of (or in addition to) attributes:\n\n```css\nphantom-ui {\n  --shimmer-color: rgba(100, 200, 255, 0.3);\n  --shimmer-duration: 2s;\n  --shimmer-bg: rgba(100, 200, 255, 0.08);\n}\n```\n\n## Custom Elements Manifest\n\nThe 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.\n\n## Bundle size\n\nThe CDN build (Lit included) is ~22kb / ~8kb gzipped.\n\nWhen used as an ES module with a bundler, Lit is likely already in your dependency tree, bringing the component cost down to under 2kb.\n\n## Development\n\n```bash\nbun install\nbun run storybook       # dev server on :6006\nbun run build           # tsc + custom elements manifest + CDN bundle\nbun run lint            # biome check\nbun run lint:fix        # biome auto-fix\n```\n\nThe `examples/` directory contains test apps for React, Vue, Solid, Angular, and Qwik, each wired to the local package.\n\n## Acknowledgements\n\nThe 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.\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faejkatappaja%2Fphantom-ui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faejkatappaja%2Fphantom-ui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faejkatappaja%2Fphantom-ui/lists"}