shadcn-html / es-modules
ES Modules
Interactive components ship as native
ES modules.
Load them with <script type="module"> — no bundler, no build step, no framework.
What is type="module"?
Adding type="module" to a <script> tag tells the browser to treat the file as an
ECMAScript module
instead of a classic script. The key differences:
Module scope, strict mode, and deferred execution — with a classic script you'd need an IIFE,
a 'use strict' directive, and a DOMContentLoaded listener to get all three.
With type="module", the browser handles them automatically.
No import, no export
You'll notice our component modules don't use import or export at all. This is intentional.
Each .js file is a side-effect module — it wraps its DOM-querying logic in an
init() function, calls it once on load, and sets up a MutationObserver to
re-run it whenever the DOM changes. There's nothing to export because the module's purpose is
its side effect: wiring behavior to HTML elements.
There are no import statements either, because each component is fully self-contained.
A dialog doesn't depend on a tooltip. A dropdown doesn't share code with a combobox.
Each component works on its own — include only the <script> tags you need.
This is the key advantage of the pattern: no bundler, no import maps, no dependency resolution.
Drop a <script type="module"> tag onto the page and the component works. Remove it
and nothing breaks. You're composing at the HTML level, not the JavaScript level.
Static pages vs. SPAs
Component JS works the same way on both static and dynamic pages — load it once and it handles the rest.
On a static page, the <script> tags run once after the DOM is ready and every component
just works. No setup, no configuration.
In an SPA or any page that dynamically updates its HTML, new elements are initialized automatically.
Each component module sets up a MutationObserver that watches the document for DOM changes
and re-runs its initialization logic. The data-init attribute on every initialized element
prevents double-binding — the :not([data-init]) selector ensures only new elements are
wired up. There is nothing to call or configure:
Which components need JS?
Most components are CSS-only. JavaScript is only used when HTML and CSS can't express the behavior — things like keyboard navigation, focus management, and state coordination.
Serving locally
ES modules require HTTP due to browser CORS policy — they won't load from file:// URLs.
Use any local server:
Browser support
ES modules have been supported in all major browsers since 2018:
See caniuse.com/es6-module and the MDN Modules guide for full details.