shadowkit
Tailwind v4 inside the Shadow DOM, the cascade-safe way.
shadowkit
Tailwind v4 inside Shadow DOM — cascade boundary holds
3 shadow roots · synced through a typed postMessage bridge
loading sk-counter…
Elevator pitch
A tiny set of helpers for building Web Components that ship with their own design system but don’t bleed into (or get poisoned by) the host page. Theme tokens scoped to
:host, cascade-safe stylesheet injection via Constructable Stylesheets with content-hash dedupe, and a typedpostMessagebridge for cross-instance state.
What it is
A Turborepo containing four small packages plus a runnable example:
@shadowkit/core—ShadowComponentbase class,createStore,watchStore,defineElement, cleanup-tracker fordisconnectedCallback.@shadowkit/theme— design-token DSL that compiles to:host { --token: value }blocks. No global CSS variables.@shadowkit/tailwind— adopted-stylesheet injector that takes the Tailwind v4 output, content-hashes it, and dedupes across instances. The same CSS source produces oneCSSStyleSheetobject across an arbitrary number of<sk-counter>instances.@shadowkit/bridge—makeCounterBridge()returning a typed pub/sub attached towindow. Lets sibling components agree on shared state without exposing internals.
The examples/embed-counter demo proves the cascade boundary: three <sk-counter> instances on a host page, the host has its own hostile theme, and host CSS rules targeting .sk-counter or sk-counter cannot reach the component’s internals.
Status
| Repo | github.com/mateokadiu/shadowkit |
| License | MIT |
| Status | shipped |
| Packages | @shadowkit/core, /theme, /tailwind, /bridge |
The problem I was solving
Every embed-on-someone-else’s-page widget hits the same two walls:
- Host CSS leaks in. Random
* { box-sizing: border-box; }rules and globala { color: blue }declarations mangle the widget’s design. - Tailwind doesn’t naturally work inside the Shadow DOM — utilities are emitted against
:root, which Shadow DOM doesn’t see.
Shadow DOM solves leak #1 if you set it up carefully. Leak #2 — Tailwind tokens — is the part most projects punt on. shadowkit lets me ship a real component with real Tailwind utilities and have it survive a host page styled in !important-stuffed legacy CSS.
Key decisions
- Constructable Stylesheets, not
<style>tags. The browser dedupes adopted stylesheets natively; sameCSSStyleSheetobject → zero extra cost across 100 instances. - Content-hash dedupe on stylesheet identity. If two callers pass the same CSS string, they get the same stylesheet handle.
- Theme tokens scoped to
:hostnot:root. Lets multiple themes coexist on one page; no global namespace. - Bridge attached to
window. Simpler than CustomEvent bubbling for cross-instance state; typed via TypeScript module augmentation.
Numbers
- 4 packages + 1 runnable example
- One CSSStyleSheet object shared across N instances of
<sk-counter>on a page - Zero host-page CSS rules can reach
<sk-counter>internals