DEV Community

Cover image for Your design system's Shadow DOM breaks skeleton loaders. Here's the fix
Aejkatappaja
Aejkatappaja

Posted on

Your design system's Shadow DOM breaks skeleton loaders. Here's the fix

If you build with a Web Component design system (Stencil, Lit, FAST, or anything built on them like Ionic and Shoelace), skeleton loaders have a quiet failure mode: the content you want to measure isn't in the light DOM. It's inside a shadow root.

Say a row in your list is <user-card>. From the outside, that element looks nearly empty: the avatar, name, and bio live inside its shadow root, projected through slots. A skeleton tool that measures the light DOM walks in, finds no leaf elements to trace, and gives you a blank or collapsed placeholder. The skeleton doesn't match because the tool literally can't see the real layout.

Why it's hard?

Shadow DOM is an encapsulation boundary on purpose. querySelectorAll from the document doesn't cross it. getComputedStyle stops at the host. And slotted content is even trickier: a <slot> renders light-DOM children at a position defined inside the shadow tree, so where a node is in the DOM and where it paints are different places.

To generate an accurate skeleton for these components you have to:

  1. Detect that a slotted element has an open shadow root
  2. Walk into that root instead of stopping at the host
  3. Resolve <slot> elements to the nodes actually projected into them
  4. Measure the real painted leaves, wherever they live

The fix

phantom-ui does this behind one attribute, pierce-shadow:

  <phantom-ui loading pierce-shadow>
    <user-card></user-card>
    <user-card></user-card>
    <user-card></user-card>
  </phantom-ui>
Enter fullscreen mode Exit fullscreen mode

With pierce-shadow on, it descends into each open shadow root follows the slots to their projected content, and measures the actual leaf elements inside.
The shimmer blocks land on the real avatar, the real heading, the real text lines, even though all of it is encapsulated.
Same structure-aware skeletons you'd get for plain markup, now for your component library.

pierce-shadow demo phantom-ui

One honest limit: this works on open shadow roots only. A closed shadow root is invisible to everything outside it by design, including this, so there's nothing to measure. Open roots are the common default, so most design systems are covered.

It composes with the rest of the library, so the same component also does the refresh/overlay mode and works across React, Vue, Svelte, Angular, Solid, Qwik and vanilla.
If you want the how-it-works on the core DOM-measurement technique, that's in the original write-up.

Try it

npm install @aejkatappaja/phantom-ui
Enter fullscreen mode Exit fullscreen mode

Or via CDN:

<script src="https://unpkg.com/@aejkatappaja/phantom-ui/dist/phantom-ui.cdn.js"></script>
Enter fullscreen mode Exit fullscreen mode

Links:

If your rows are custom elements and your skeletons never looked right, flip pierce-shadow on.

Top comments (0)