π Ship Preact micro-components anywhere. Zero config. Under 1KB.
Features β’ Quick Start β’ Examples β’ API β’ Why?
|
Under 1KB min+gzip. Your widget code will be bigger than the framework. Optional style encapsulation. Your CSS stays in, theirs stays out. Defer rendering until visible with IntersectionObserver. Ship fast, load smart. |
Register as native Custom Elements. Works everywhere, no framework required. Optional First-class server-rendering support. Fast to paint, fast to interact. |
npm install preact-embed preactyarn add preact-embed preactpnpm add preact-embed preact// widgets/PricingCard.tsx
import { h } from 'preact';
import { useState } from 'preact/hooks';
interface Props {
plan?: string;
price?: number;
features?: string[];
}
export function PricingCard({ plan = 'Pro', price = 29, features = [] }: Props) {
const [selected, setSelected] = useState(false);
return (
<div className="pricing-card">
<h2>{plan}</h2>
<p className="price">${price}/mo</p>
<ul>
{features.map(f => <li key={f}>{f}</li>)}
</ul>
<button onClick={() => setSelected(!selected)}>
{selected ? 'β Selected' : 'Choose Plan'}
</button>
</div>
);
}// index.tsx
import embed from 'preact-embed';
import { PricingCard } from './widgets/PricingCard';
const { render } = embed(PricingCard);
render({
selector: '[data-widget="pricing"]',
defaultProps: {
plan: 'Starter',
price: 9,
},
});<!-- On any website, CMS, or app -->
<div
data-widget="pricing"
data-prop-plan="Enterprise"
data-prop-price="99"
>
<script type="application/json">
{
"features": ["Unlimited users", "Priority support", "Custom integrations"]
}
</script>
</div>
<script src="https://cdn.example.com/pricing-widget.js" async></script>That's it. Your Preact component now runs anywhere. π
π Basic Selector Mount
import embed from 'preact-embed';
import { NewsletterForm } from './NewsletterForm';
const { render } = embed(NewsletterForm);
render({
selector: '#newsletter-signup',
});<div id="newsletter-signup"></div>π Shadow DOM Isolation
Perfect for third-party embeds where you can't control the host page's CSS.
render({
selector: '[data-widget="checkout"]',
shadowRoot: true, // Styles won't leak in or out
});
// Or with options
render({
selector: '[data-widget="checkout"]',
shadowRoot: { mode: 'closed' }, // Extra privacy
});π¦₯ Lazy Loading
Only load widgets when they scroll into view.
render({
selector: '.below-the-fold-widget',
lazy: true, // Uses IntersectionObserver
});
// With custom threshold
render({
selector: '.below-the-fold-widget',
lazy: {
threshold: 0.5, // 50% visible before loading
rootMargin: '100px', // Start loading 100px before visible
},
});π§© Custom Elements (Web Components)
Register your widget as a native HTML element.
import embed from 'preact-embed';
import { VideoPlayer } from './VideoPlayer';
const { render } = embed(VideoPlayer);
render({
tagName: 'x-video-player', // Registers <x-video-player>
shadowRoot: true,
});<!-- Now use it like native HTML! -->
<x-video-player
src="https://example.com/video.mp4"
autoplay="false"
poster="thumbnail.jpg"
></x-video-player>π§ SSR Hydration
Hydrate server-rendered markup instead of replacing it.
render({
selector: '[data-widget="comments"]',
hydrate: true, // Attaches to existing DOM
});π‘ Cross-Widget Communication
Widgets can talk to each other through the event bus.
// Widget A: User Selector
import { eventBus } from 'preact-embed';
function UserSelector() {
const selectUser = (user) => {
eventBus.emit('user:selected', user);
};
// ...
}
// Widget B: User Profile (different widget, same page)
import { useHabitatEvent } from 'preact-embed';
function UserProfile() {
const [user] = useHabitatEvent('user:selected');
return user ? <Profile user={user} /> : <p>Select a user</p>;
}β‘ Signals Integration
Share reactive state across widgets without prop drilling.
// Install: npm install @preact/signals
import { getSignal, createStore } from 'preact-embed/signals';
// Global reactive counter (accessible from any widget)
const count = getSignal('cart-count', 0);
function AddToCartButton({ productId }) {
const addToCart = () => {
count.value++;
// All widgets using this signal will update!
};
return <button onClick={addToCart}>Add to Cart ({count})</button>;
}
function CartIcon() {
return <span className="cart-badge">{count}</span>;
}Creates an embed instance for a Preact component.
import embed from 'preact-embed';
const { render, renderOne, unmountAll, instances } = embed(MyWidget);| Return | Type | Description |
|---|---|---|
render |
(options) => WidgetInstance[] |
Mount to all matching elements |
renderOne |
(element, props?) => WidgetInstance |
Mount to a single element |
unmountAll |
() => void |
Destroy all widget instances |
instances |
WidgetInstance[] |
Array of active instances |
interface EmbedOptions<P> {
// Target elements
selector?: string; // CSS selector to find mount points
inline?: boolean; // Mount in parent of <script> tag
clientSpecified?: boolean; // Read selector from data-mount-in attr
// Props
defaultProps?: Partial<P>; // Default props for all instances
// Rendering
clean?: boolean; // Clear innerHTML before mounting
hydrate?: boolean; // Hydrate SSR markup instead of render
// Advanced
shadowRoot?: boolean | ShadowRootInit; // Enable Shadow DOM
lazy?: boolean | IntersectionObserverInit; // Lazy load on scroll
tagName?: string; // Register as Custom Element
// Lifecycle
onBeforeMount?: (el: Element, props: P) => void | P;
onMounted?: (el: Element, props: P) => void;
onUnmount?: (el: Element) => void;
}interface WidgetInstance<P> {
element: Element; // The mounted DOM element
props: P; // Current props
update: (props: Partial<P>) => void; // Update props & rerender
unmount: () => void; // Destroy this instance
}<div
data-widget="pricing"
data-prop-plan="Pro"
data-prop-price="29"
data-prop-is-popular="true"
data-prop-discount-percent="20"
></div>| Attribute | Prop | Value |
|---|---|---|
data-prop-plan |
plan |
"Pro" |
data-prop-price |
price |
29 (number) |
data-prop-is-popular |
isPopular |
true (boolean) |
data-prop-discount-percent |
discountPercent |
20 (number) |
Auto-parsing: Numbers, booleans (
true/false),null, and JSON are automatically parsed.
<div data-widget="pricing">
<script type="application/json">
{
"plan": "Enterprise",
"price": 99,
"features": ["SSO", "Audit logs", "99.99% SLA"],
"contact": {
"email": "sales@example.com",
"phone": "1-800-EXAMPLE"
}
}
</script>
</div>Tip: JSON props are great for arrays, objects, and complex data structures.
Vite
// vite.config.ts
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
export default defineConfig({
plugins: [preact()],
build: {
lib: {
entry: 'src/index.tsx',
name: 'MyWidget',
formats: ['umd', 'es'],
fileName: (format) => `widget.${format}.js`,
},
rollupOptions: {
output: {
globals: {
preact: 'preact',
},
},
},
},
});Webpack
// webpack.config.js
module.exports = {
entry: './src/index.tsx',
output: {
filename: 'widget.js',
library: {
name: 'MyWidget',
type: 'umd',
},
globalObject: 'this',
},
// ... rest of config
};tsup
// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.tsx'],
format: ['cjs', 'esm', 'iife'],
globalName: 'MyWidget',
minify: true,
dts: true,
});| Module | Size (min+gzip) |
|---|---|
preact-embed |
~1.0 KB |
preact-embed/signals |
~0.5 KB |
preact-embed/lite |
~0.5 KB |
Compare that to your alternatives:
| Library | Size |
|---|---|
| preact-embed | ~1.0 KB |
| preact-island | ~1.3 KB |
| preact-habitat | ~0.9 KB |
| React + ReactDOM | ~45 KB |
You need to embed a Preact component into:
- A client's WordPress site
- A Shopify storefront
- An existing jQuery app
- Any CMS or third-party website
But you can't control the build system, the CSS, or what else is on the page.
preact-embed gives you a bulletproof way to:
- Mount anywhere β By selector, inline, or as a Web Component
- Stay isolated β Shadow DOM keeps your styles safe
- Load smart β Lazy loading for below-the-fold widgets
- Communicate β Event bus and Signals for multi-widget pages
- Stay tiny β Under 1KB, so Preact itself is your biggest dependency
Already using preact-habitat? The API is nearly identical:
- import habitat from 'preact-habitat';
+ import embed from 'preact-embed';
- const { render } = habitat(MyWidget);
+ const { render } = embed(MyWidget);
render({
selector: '.my-widget',
+ // New features available:
+ shadowRoot: true,
+ lazy: true,
});| Browser | Version |
|---|---|
| Chrome | 60+ |
| Firefox | 55+ |
| Safari | 12+ |
| Edge | 79+ |
Note: For IE11 or older browsers, you'll need polyfills for
IntersectionObserver(lazy loading) andcustomElements(Web Components).
Contributions are welcome! Please read our Contributing Guide first.
# Clone the repo
git clone https://github.com/openconjecture/preact-embed.git
# Install dependencies
pnpm install
# Run tests
pnpm test
# Build
pnpm buildMIT Β© Garrett Eastham
Built with β€οΈ for the Preact community
