npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ariel-salgado/vite-plugin-shadow-dom

v0.1.0

Published

Vite plugin that isolates your app into a Shadow DOM — with zero changes to your application code

Readme

@ariel-salgado/vite-plugin-shadow-dom

A Vite plugin that wraps your app inside a Shadow DOM, providing true style and DOM encapsulation with no changes to your application code.

Note: This plugin is designed and tested for vanilla HTML, JavaScript, and TypeScript projects. Framework support has not been tested.

Motivation

When embedding a Vite app inside a third-party page or a legacy document, the host page's styles inevitably bleed into your app — and yours leak out. Shadow DOM is the platform's native solution to this problem. This plugin handles all the wiring so your app runs in a fully isolated shadow tree without you having to restructure anything.

How it works

The plugin finds the element with id="app" in your HTML, moves it into a <template> tag, replaces it with a shadow host <div>, and injects a bootstrap <script> that attaches a shadow root and stamps the template into it at runtime.

Everything else in <body> — any headers, footers, or scripts outside #app — is left exactly where it is in the regular document.

Given this input:

<body>
  <div id="app">
    <h1>Hello</h1>
  </div>
  <script type="module" src="/src/main.ts"></script>
</body>

The plugin produces:

<body>
  <div id="shadow-host" style="display:block;width:100%;height:100%"></div>

  <template id="shadow-template">
    <div id="app">
      <h1>Hello</h1>
    </div>
  </template>

  <script type="module">
    const host = document.getElementById('shadow-host');
    const shadow = host.attachShadow({ mode: 'open', delegatesFocus: true, serializable: true });

    const tpl = document.getElementById('shadow-template');
    shadow.appendChild(tpl.content.cloneNode(true));

    window['__shadowRoot'] = shadow;

    // Patch document query methods to search the shadow root first
    for (const m of ['getElementById', 'querySelector', 'querySelectorAll']) {
      const orig = document[m].bind(document);
      document[m] = (...args) => {
        const r = shadow[m](...args);
        return r != null && (!('length' in r) || r.length > 0) ? r : orig(...args);
      };
    }

    // Mirror dev-mode <style> injections into the shadow root
    new MutationObserver(mutations => {
      for (const { addedNodes } of mutations)
        for (const node of addedNodes)
          if (node.nodeName === 'STYLE') shadow.appendChild(node.cloneNode(true));
    }).observe(document.head, { childList: true });

    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = '/assets/index.css';
    shadow.appendChild(link);

    import('/assets/index.js');
  </script>
</body>

Installation

# npm
npm install -D @ariel-salgado/vite-plugin-shadow-dom

# pnpm
pnpm add -D @ariel-salgado/vite-plugin-shadow-dom

# bun
bun add -D @ariel-salgado/vite-plugin-shadow-dom

Requires Vite >= 7.0.0

Usage

import { shadowDOM } from '@ariel-salgado/vite-plugin-shadow-dom';
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [shadowDOM()],
});

With options:

import { shadowDOM } from '@ariel-salgado/vite-plugin-shadow-dom';

export default defineConfig({
	plugins: [
		shadowDOM({
			mode: 'closed',
			cssStrategy: 'constructable',
			formatOutput: false,
		}),
	],
});

See the documentation for a full description of every option.

Dev mode

The plugin runs in both dev and production. During development, Vite injects CSS as <style> elements directly into document.head at runtime rather than emitting <link> tags. The bootstrap script installs a MutationObserver on document.head that automatically clones any <style> tag into the shadow root as it appears, so hot module replacement and style updates work without any extra configuration.

CSS scoping

Built CSS files are injected into the shadow root as <link> tags (or as CSSStyleSheet objects when cssStrategy: 'constructable' is set), so all your styles are scoped to the shadow tree. CSS files are also kept in <head>, which ensures document-level selectors like :root and body continue to work as expected.

Closed mode

When mode: 'closed', element.shadowRoot returns null from outside the shadow tree. The plugin exposes the shadow root on window.__shadowRoot before importing your app's JS so it is always accessible regardless of mode.

// Access the shadow root directly if needed
const shadow = window.__shadowRoot;
const app = shadow.getElementById('app');

The window property name is configurable via the shadowRootGlobal option.