Build standalone, embeddable widgets in React.
Write a React component — ship a self-contained bundle that mounts onto any website as a Shadow-DOM custom element with fully isolated CSS (Tailwind v4 included).
Status: alpha (pre-1.0). The runtime, build preset, and CLI work, are covered by three examples, and the packages build to
dist(npm-ready). Not yet published — the remaining steps are the npm scope + release automation (docs/PUBLISHING.md). APIs may change before 1.0.
Shipping a widget that lives on someone else's site — a support bubble, a pricing table, a feedback button — means hand-rolling the same fragile stack every time: a Vite library build, a custom element, a Shadow root, CSS isolation, and runtime config plumbing. And Tailwind v4 silently renders unstyled inside a shadow root unless you know the fix.
BundleHive does all of that once, so you just write React.
your React component ──► bundlehive build ──► <my-widget></my-widget>
<script src="…/embed.js" async>
- 🧩 Write plain React — no custom-element or Shadow-DOM boilerplate.
- 🎨 Real CSS isolation — styles scoped to a shadow root, host CSS locked out.
Tailwind v4 works inside the shadow root (we handle the
@property/:root→:hostfix). - 🚀 One build → two outputs — an IIFE bundle for
<script>/CDN embedding and an ESM package for npm consumers. - 🔌 Three embed modes — placed element, self-injecting launcher, or a command-queue JS API (Intercom-style).
- ⚙️ Runtime config — one bundle serves every tenant and environment; no rebuild per key.
- 🛠️ Zero-config CLI —
bundlehive devandbundlehive build, novite.config.ts.
npm install @usereq/bundlehive react react-dom
npm install -D @usereq/bundlehive-cli tailwindcsssrc/widget.tsx — a plain component:
import { useState } from "react";
import { useWidget } from "@usereq/bundlehive";
export function Counter() {
const { config } = useWidget<{ start?: string }>();
const [n, setN] = useState(Number(config.start ?? "0") || 0);
return (
<button
onClick={() => setN(n + 1)}
className="rounded-full bg-indigo-600 px-4 py-2 font-medium text-white"
>
Clicked {n} times
</button>
);
}src/embed.ts — register it (importing this defines <bh-counter>):
import { defineWidget } from "@usereq/bundlehive";
import styles from "./styles.css?inline"; // @import "tailwindcss";
import { Counter } from "./widget";
export default defineWidget(Counter, {
tag: "bh-counter",
styles,
observedAttributes: ["start"],
});Build and embed:
bundlehive build # → dist/embed.js (IIFE) + dist/embed.mjs (ESM)<bh-counter start="10"></bh-counter>
<script src="https://unpkg.com/@acme/counter" async></script>The <script> can live in <head> with async — a custom element upgrades in
place whenever the browser parses it, regardless of load order.
| Mode | Customer pastes | Use for |
|---|---|---|
| Placed element | a tag + script | config via string attributes, in a known spot |
Auto-inject (autoMount: true) |
just the script | a floating launcher that injects itself |
Command-queue loader (createLoader) |
a stub + script + JS calls | rich config, per-tenant keys, imperative open/close |
See embed-modes.md for full examples of each.
Mounting in a shadow root keeps the host page's CSS out and your CSS in — but
Tailwind v4 declares theme variables on :root (which doesn't match a shadow
host) and registers typed properties with @property (which browsers ignore
inside shadow roots). BundleHive rewrites :root→:host, hoists @property
rules to the document, and adopts one shared constructable stylesheet per widget.
You just @import "tailwindcss" and import it ?inline — the runtime does the rest.
bundlehive dev # HMR playground
bundlehive build # embed bundle → dist/ (--name, --entry, --external, --outDir, --port)| Package | Purpose |
|---|---|
@usereq/bundlehive |
Runtime + authoring API (defineWidget, useWidget, Portal, createLoader) |
@usereq/bundlehive-cli |
bundlehive dev / bundlehive build |
@usereq/bundlehive-build |
The Vite build preset (used by the CLI) |
examples/counter— placed element, attributes,<Portal>examples/floating-badge— self-injecting launcherexamples/loader-chat— command-queue loader
bun install
bun --filter '@usereq/bundlehive-example-counter' dev # then open the printed URL- llms.txt — agent-readable reference
- Agent skill —
.claude/skills/bundlehive-widget(API · modes · publishing) - docs/PLAN.md — scope, research, roadmap
- docs/PUBLISHING.md — how releases work
Not a meta-framework. No SSR, no routing, no server — it's a bundler + thin runtime for client-rendered embeds. For your own app's pages, reach for Next.js or Vite.
MIT