---
title: Quick Start
description: Render a Framewerk screen in one HTML file, then change it, embed it in React, and have an agent generate one.
status: shipped
url: https://getairframe.com/framewerk/getting-started/quick-start
---

> Part of the Framewerk docs. Index of every page: /framewerk/llms.txt

# 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.

```html
<!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):

```json
{
  "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

```sh
npm install @framewerk/renderer @framewerk/spec react react-dom
```

```tsx
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](/framewerk/getting-started/skills.md) and [Agents & MCP](/framewerk/integrations/agents-and-mcp.md).

---

Next: [Core concepts](/framewerk/core/document.md) · [Node reference](/framewerk/reference/nodes.md) · [Playground](https://airframe.to/framewerk/playground)
