UI that pops up when you need it.
An open, secure-by-design standard for isolated, pluggable interactive UI.
What is a Bubble?
One UI instance of an extension, shown inside a host surface.
The host decides where a Bubble appears. The extension decides what is inside it. Neither has to know how the other is built.
- Isolated
- Each extension runs apart from the host and from other extensions. It sees only what the user hands it.
- Independent
- A host does not need to ship a calendar to show one. An extension supplies it, and the host brokers every call.
- Not only chat, not only AI
- Chat is the first surface. A dashboard, an IDE sidebar or a terminal can host Bubbles too, and an assistant, a button or plain code can pop one.
Three ways to render
A mode says how a Bubble draws itself. It never implies a permission.
Declarative
Data only. The host renders an HTML subset itself, so no code from the extension runs. It is also the fallback for text and terminal hosts.
Trade-off: safest by construction, limited to what the subset can express.
Worker
Third-party code. One hidden sandboxed iframe per extension, one Web Worker per Bubble. The Bubble's DOM is mirrored into a Shadow DOM in the host through an allowlist sanitizer, with CSS containment.
Trade-off: light, themable and accessible, but weaker isolation than an iframe per Bubble. A sanitizer bug means code runs on the host's origin.
Frame
A visible iframe, for full existing web apps. Optional in the spec.
Trade-off: the strongest boundary, but each Bubble boots a whole document, sizing inside a chat is fiddly and gluing is awkward.
Extensions call each other
Through the host, by id. Not through a fixed list of platform APIs.
A tax calculator wants to share a result with people. It asks for a contacts picker, and the user chooses.
Draft syntax, will change// In the caller's manifest: a local alias, like an import map
"uses": {
"contacts": { "id": "https://contacts.example.com/", "version": "^1" }
}
// In the caller's code
const picked = await uib.ext("contacts").choose({
min: 1, max: 5, fields: ["name", "email"],
});
// picked holds only what the user ticked, only those fieldsInteractive calls open the callee's Bubble on top of the caller, labelled with who is asking. They must start from a user gesture, and the caller gets only what was picked. Headless calls return data without UI, so the host checks a grant first.
An extension's id is any https directory URL that contains uibubbles.json. DNS is the registry. Read how ids work.
Drag, drop and glue
Bubbles stay isolated, yet they can work together.
Drag and drop. Drag one item from a Bubble onto another, like a contact onto a meeting. The host runs the drag: the source offers a typed item, and only Bubbles that declare they accept that type light up as drop targets. The drop is the consent, so the receiver gets exactly that item and nothing else. Keyboard and touch work too.
Glue. Two touching Bubbles form a wall that the host owns. Only events both sides declared can cross it: selecting a meeting can highlight its attendees, and picking a person can filter the calendar.
event.selected crosses the wall.A cluster of glued Bubbles is called foam. Foam is plain data, so it can be pinned or shared as a small app without anyone writing code. You can unglue it, and popping one Bubble leaves the rest.
Draft syntax, will change{
"foam": "Family week",
"members": ["calendar", "to-buy", "contacts"],
"walls": [
{ "between": ["calendar", "contacts"], "events": ["event.selected", "contact.selected"] }
]
}Security first
The design starts from what a Bubble must not be able to do.
A Bubble declares what it wants. The Host decides what it gets.
The user's gesture is the grant.
What the host decided for one Bubble
- Granted: theme
- Granted: timezone
- Asks the user each time: contacts (via picker)
- Denied: unrestricted network
- The Bubble's manifest lists what it asks for. Asking is not getting.
- The host evaluates the request.
- The user's gesture, such as picking a contact, or a policy grants a slice.
- The Bubble receives only that slice.
Honest limit. Worker mode renders into the host's own DOM, which is weaker isolation than one iframe per Bubble. The sanitizer is the security-critical part and needs adversarial tests and outside review before any third-party extension ships. Nothing here is implemented or audited yet. Read the threat model sketch.
How it compares
UIBubbles aims to use existing work, not replace it.
| Project | What it does well | What UIBubbles adds |
|---|---|---|
| MCP Apps | A tool result renders as a sandboxed iframe with host context. | Calls between apps and gluing. An app can use the host's and other extensions' UI. |
| A2UI and OpenUI | Safe declarative UI that an agent generates. | Third-party code and extension-to-extension calls. Their formats may inform Declarative mode. |
| Facebook and VK style apps | Apps call platform APIs, including user pickers. | APIs supplied by other extensions instead of one platform, and lighter Bubbles than one heavy iframe per app. |
| Shopify remote-dom, AMP worker-dom | Isolated code whose UI is mirrored into the host. | The rendering technique UIBubbles reuses. No calls or gluing there. |
More in prior art.
Status and how to help
Early draft. Governance is undecided.
UIBubbles is a recorded idea with a draft manifest schema and draft docs. There is no released runtime. A first host is in development.
Who runs a public standard, and under what licence, has not been decided. Code is provisionally Apache-2.0 and documentation CC-BY-4.0.
The most useful things right now: try to break the security model, argue a position on an open question, or tell us what you would build as an extension.