⬡ FRAMEWERK
Getting started

Quick Start

Render a Framewerk screen in one HTML file, then change it, embed it in React, and have an agent generate one.

View as Markdown

Quick Start

Four steps: render a spec → change it → install in a React app → have an agent generate one. Each step is one code block.

1. Render a screen — one HTML file, no build

Save as index.html and open it over HTTP (not file:// — ES modules need an origin). Source: the no-build example, pointed at the published packages.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Framewerk — no build step</title>
    <script type="importmap">
      {
        "imports": {
          "react": "https://esm.sh/react@18.3.1",
          "react/jsx-runtime": "https://esm.sh/react@18.3.1/jsx-runtime",
          "react-dom/client": "https://esm.sh/react-dom@18.3.1/client?external=react",
          "lucide-react": "https://esm.sh/lucide-react@0.574.0?external=react",
          "@framewerk/spec": "https://esm.sh/@framewerk/spec",
          "@framewerk/spec/fixtures": "https://esm.sh/@framewerk/spec/fixtures",
          "@framewerk/renderer": "https://esm.sh/@framewerk/renderer?external=react"
        }
      }
    </script>
  </head>
  <body>
    <div id="root" style="width:393px;min-height:500px;border:1px solid #ddd"></div>
    <script type="module">
      import { createElement as h, useState } from "react";
      import { createRoot } from "react-dom/client";
      import { RendererProvider, NodeRenderer, useRenderer } from "@framewerk/renderer";
      import { sampleSpec } from "@framewerk/spec/fixtures";

      function Screen({ onNav }) {
        const { state } = useRenderer();
        const screen = state.spec.screens?.find((s) => s.id === state.currentScreen);
        if (!screen?.layout) return null;
        return h(NodeRenderer, {
          node: screen.layout,
          comps: state.spec.components ?? {},
          path: [],
          stateValues: state.previewStateValues,
          onNav,
        });
      }

      // The provider holds no navigation state — the host owns screenId.
      function App() {
        const [screenId, setScreenId] = useState("menu");
        return h(
          RendererProvider,
          { spec: sampleSpec, screenId, fontStrategy: "none" },
          h(Screen, { onNav: setScreenId }),
        );
      }

      createRoot(document.getElementById("root")).render(h(App));
    </script>
  </body>
</html>

Serve it (npx serve .) and you get a working two-screen prototype: tap Order to navigate, Toggle oat milk to flip a state variable. The ?external=react on the CDN URLs is load-bearing — without it you get two Reacts on one page.

2. Change it — state, an action, a navigate

A spec is plain JSON. Add a screen with a state variable and wire buttons to actions (source: the format reference):

{
  "formatVersion": "1.0",
  "screens": [
    {
      "id": "menu",
      "stateVars": { "oatMilk": { "type": "boolean", "default": "false" } },
      "layout": {
        "_id": "menu-root",
        "type": "column",
        "gap": 16,
        "padding": 24,
        "children": [
          { "_id": "menu-title", "type": "text", "variant": "heading", "content": "Coffee Cart" },
          { "_id": "menu-oat", "type": "button", "content": "Toggle oat milk", "action": "toggleState(oatMilk)" },
          { "_id": "menu-badge", "type": "badge", "content": "Oat milk", "showWhen": "$oatMilk == true" },
          { "_id": "menu-order", "type": "button", "variant": "primary", "content": "Order", "action": "navigate(order)" }
        ]
      }
    }
  ]
}

toggleState and setState work out of the box. navigate() and back() are reported through callbacks — your host owns the screen id, exactly as in step 1.

3. Install in a React app

npm install @framewerk/renderer @framewerk/spec react react-dom
import { useState } from "react";
import { RendererProvider, NodeRenderer, useRenderer } from "@framewerk/renderer";
import { sampleSpec } from "@framewerk/spec/fixtures";

function Screen({ onNav }: { onNav: (id: string) => void }) {
  const { state } = useRenderer();
  const screen = state.spec.screens?.find((s) => s.id === state.currentScreen);
  if (!screen?.layout) return null;
  return (
    <NodeRenderer
      node={screen.layout}
      comps={state.spec.components ?? {}}
      path={[]}
      stateValues={state.previewStateValues}
      onNav={onNav}
    />
  );
}

export default function App() {
  const [screenId, setScreenId] = useState("menu");
  return (
    <RendererProvider spec={sampleSpec} screenId={screenId} fontStrategy="none">
      <Screen onNav={setScreenId} />
    </RendererProvider>
  );
}

Source: the embed example. Swap sampleSpec for your own Spec when you have one.

4. Have an agent generate one

Point a coding agent at the format reference and ask for a screen:

Write a Framewerk spec with two screens: home (a heading, a search input, and a primary "Search" button) and results (a list of three rows). The button navigates to results. Validate it with validate() from @framewerk/spec.

Most agents that can read the format reference emit a valid document; the validator catches the ones that don't. For a maintained agent setup, see Skills and Agents & MCP.


Next: Core concepts · Node reference · Playground