Ship a product as one polished shell, but build it as many small apps that teams
own end to end. Frame is the micro-frontend architecture that makes that
possible: a host page composes independent web apps into a single experience,
and a typed SDK keeps the two sides talking — without ever sharing a bundle.
Published as @zomme/frame, it powers the micro-frontend shell in
Buntime.
The problem it solves
Splitting a frontend into independent apps usually means giving up isolation or giving up integration. Share a bundle and one team’s upgrade breaks another’s build; isolate everything in iframes and you lose the typed, two-way communication a real product needs. Frame keeps both: each app stays a separate deployable behind an iframe, while a typed channel lets the host and child pass props, call methods and exchange events as if they were one app.
How it works
flowchart LR host["Host page · z-frame"] -->|"props · theme · apiUrl"| child["Embedded app · frame SDK"] child -->|"events · emit (postMessage)"| host
The host declares a <z-frame> element pointing at a child app, sets props,
calls methods and listens for events. The child imports the SDK, initializes,
reads its props (including callbacks) and emits or listens back to the host.
// Host page
const frame = document.querySelector("z-frame");
frame.apiUrl = "https://api.example.com";
frame.theme = "dark";
frame.onSuccess = user => console.log("created", user);
frame.addEventListener("ready", () =>
frame.emit("theme-change", { theme: "dark" })
);
frame.addEventListener("user-created", e => console.log(e.detail));
// Child app
import { frameSDK } from "@zomme/frame/sdk";
await frameSDK.initialize();
frameSDK.props.onSuccess({ id: 1 });
frameSDK.on("theme-change", ({ theme }) => applyTheme(theme));
frameSDK.watch(["apiUrl", "theme"], changes => reconfigure(changes));
frameSDK.emit("user-created", { id: 1 });
<z-frame
name="admin"
src="https://apps.example.com/admin"
base="/admin"
></z-frame>
What you get
- Host element —
<z-frame name src base>: set props (frame.apiUrl,frame.theme,frame.onSuccess), call methods (frame.emit('theme-change', {...}), with camelCase aliases likeframe.themeChange({...})), and listen to events (frame.addEventListener('ready', ...), plus custom ones like'user-created'). - Child SDK —
@zomme/frame/sdk:await frameSDK.initialize(), readframeSDK.props(including callbacks),frameSDK.emit()/frameSDK.on(), andframeSDK.watch([...], changes => {...})to react to prop changes. - Base-path injection — embedded SPAs keep working under a sub-path, so a
child app served at
/adminroutes correctly without per-app rewrites.
Stack
A Bun monorepo in TypeScript, with example apps (including Vue) building to static bundles — proof the host stays framework-agnostic while each child picks its own.
Availability
- Source: github.com/djalmajr/frame
- Related: Buntime — uses Frame as its shell
Open source under the Apache 2.0 license.