Native UI without shipping a framework
UI as data

Modules describe their UI - Mutka draws it

An untrusted module runs in a Web Worker, so it can never hand a React component across the boundary. Instead it sends a serializable tree of nodes, and the host renders them with the same native Liquid Glass widgets the core uses. Modules ship logic, not markup.

You can't postMessage a component

A community module lives behind an isolation boundary: no DOM, no document, no way to mount a React tree into Mutka's window. That isolation is the whole point of the sandbox - but UI still has to come from somewhere. Everything crossing a worker boundary must survive the structured clone algorithm, and a component can't.

So a module never sends UI. It sends a description of UI: a small, serializable UINode tree. The host receives that JSON and renders it with its own trusted, on-brand components. The module decides what; Mutka decides how it looks - the same declarative idea behind SwiftUI, applied across a process boundary.

One tree, four surfaces

The same UINode vocabulary fills every place a module can show UI, so there's only one thing to learn. Declare a panel and fill it from setup with host.ui.render(id, node); open a modal with host.ui.modal(node); add a settings section the same way.

Status-bar items are dynamic - upsert one with host.statusbar.set(item) and a click can open a popover built from the very same node tree.

  • Side panels - declarative panes docked left or right
  • Modals & settings sections - same nodes, different surface
  • Status-bar items with popovers - live, upsertable

How a click gets back to the module

Rendering is one half; interaction is the other.

  1. 01

    Describe

    The module builds a UINode tree - a button, a list, a form - and hands it to host.ui.render. Every interactive node carries an action id.

  2. 02

    Render

    The host draws the tree with native Liquid Glass widgets. The module never injects HTML, CSS or components - only JSON crosses the boundary.

  3. 03

    Route back

    When the user clicks or submits, the host routes the event by its action id back into the module's runtime, where the handler you registered with host.onUIEvent runs.

Forms are just a schema

A form is one form node carrying a FormSchema - a JSON-Schema Draft-7 subset, the standard wire format. The host renders the inputs, validates, and returns the collected values to your handler.

Authors can generate that schema from zod (z.toJSONSchema()); the host never imports zod, it just reads the schema. You describe the fields; Mutka builds the native form.

What modules render this way

Real dev-modules already use the declarative surface end to end.

Settings panes

The WebDAV module renders its whole connection form as a declarative settings section.

Inspector panels

Folder Inspector fills a side pane with a live, declarative summary of the current directory.

Status items

Dir Stats upserts a status-bar item whose popover is built from the same node tree.

Modals

Any module can pop a Liquid Glass modal with host.ui.modal(node) and close it with null.

Frequently asked

Why can't modules just use React?

Untrusted modules run in an isolated Web Worker with no DOM access - that isolation is what makes a denied permission physically unreachable. A React tree can't cross postMessage, so modules send a serializable UINode tree the host renders instead.

Can a module inject custom CSS or HTML?

No. Only JSON crosses the boundary. The host renders every node with its own trusted Liquid Glass components, so modules can't break the look or smuggle in markup.

How do forms work without a UI framework?

A form node carries a JSON-Schema (Draft-7 subset). The host renders the fields, validates input and returns the values. You can author the schema by hand or generate it from zod.

Sources & further reading

The standards and prior art behind UI-as-data.

  1. 1
    Structured clone algorithmMDN Web Docs

    The rule that defines what can cross a worker boundary, and why a UINode tree can but a React component can't.

  2. 2
    JSON Schema Draft-07JSON Schema

    The standard Mutka's FormSchema is a subset of: forms are described in an open wire format, not a proprietary one.

  3. 3
    SwiftUIApple Developer

    Apple's declarative-UI framework, the design lineage of describing interfaces as data.

  4. 4
    Zodzod.dev

    The TypeScript schema library module authors can use to generate their form schemas.

Paint a panel without a framework

Describe your UI as nodes and let Mutka render it natively.