π What is Shadow DOM?
Shadow DOM is a browser feature that allows us to attach a separate DOM tree to an element, a tree thatβs isolated from the rest of the page.
Think of it as a "mini web page" inside an element, with its own HTML, CSS, and JS completely encapsulated from the rest of the site.
π Key Concepts
- Shadow Host: The DOM element where the Shadow DOM is attached.
-
Shadow Root: The root of the shadow tree, created with
element.attachShadow(options)
. - Shadow Boundary: The barrier between the shadow DOM and light DOM (regular DOM). Styles and events generally donβt cross this boundary unless configured.
π§΅ Modes: open
vs closed
You can attach a shadow root in two modes:
-
open
: The shadow root is accessible viaelement.shadowRoot
. -
closed
: The shadow root is not accessible from the outside.
π Note: Even in
open
mode, accessing shadow DOM externally is discouraged as it breaks encapsulation.
π Communicating with the Light DOM
Since shadow DOM is isolated, direct interaction with outside elements is limited. The recommended way to communication is using Custom Events.
const voteEvent = new CustomEvent('simplepoll-vote', {
detail: { vote: 'React' },
bubbles: true,
composed: false // set true if event is dispatch within the shadow boundary.
});
host.dispatchEvent(voteEvent);
β οΈ Without
composed: true
, the event will not escape the shadow DOM and cannot be listened to in the main document.
π Important Clarification
If you dispatch the event from the shadow root, composed: true
is necessary to make it cross the boundary.
However, if you dispatch the event from the host element, composed
is not needed as itβs already outside the shadow tree.
π§ͺ Let's Build a Real Plugin: Poll Widget
To reinforce our understanding, I built a simple poll plugin using Shadow DOM. This plugin:
- Creates a styled voting widget inside a Shadow DOM
- Encapsulates its layout and logic
- Dispatches a custom event on vote
- Can be embedded into any website
π Folder Structure
poll-plugin/
βββ index.js # Entry point (exports the class)
βββ lib/
β βββ dom.js # DOM creation helpers
β βββ template.js # HTML content generator
β βββ style.js # CSS as JS string
β βββ logic.js # Voting logic and event binding
βββ index.html # to test the plugin
βββ vite.config.js # build config
βββ package.js
βοΈ How It Works
const container = createContainer('poll-container');
const shadow = container.attachShadow({ mode: 'closed' });
shadow.appendChild(style);
shadow.appendChild(wrapper);
- The poll UI is created inside the shadow root.
- Created the poll in closed mode so main dom cannot access it.
- Styles are injected with a style tag.
- The HTML is dynamically created from config.
- When a user votes, we emit a custom event which will listen by main dom.
π¬ Listening for Poll Votes
To listen event in the main dom:
document.addEventListener('vote-submitted', (e) => {
alert(`You voted: ${e.detail.vote}`);
});
This keeps your main app clean while the logic is self-contained.
π Bundling with Vite
To make this plugin usable in any website, I used Vite as it's a fast, modern frontend build tool that simplifies development and bundling.Vite allows us to:
- Write modular, clean source code in separate files
- Use modern JavaScript or TypeScript
- Compile everything into a single minified JS file ready for production
π How to Use It in Any Website
After bundling the plugin we can easily embedd to any website using script tag.
<script src="./dist/poll.iife.js"></script>
<script>
Poll.init({
question: 'What is your favorite JS framework?',
options: ['React', 'Vue', 'Svelte', 'Solid']
});
document.addEventListener('vote-submitted', (e) => {
alert(`You voted: ${e.detail.vote}`);
});
</script>
It works ansywhere without leaking styles or affecting the main page.
Top comments (0)