# Introduction
> MorphScreen lets a model answer with real UI. It reads the answer as it streams, checks every line against the components your app allows, and draws it on the page.
A saved answer from DeepSeek V4 Flash, unedited, streaming into a page through the web SDK at the pace the model wrote it.
A model writes its answer as Markdown with a few tags, like ``, ``, `` and `
```
```ts
import { connectAgent, loadCore } from "@morphscreen/web";
import "@morphscreen/web/morphscreen.css";
// morphscreen-agent.wasm from the package, served with your page
const core = await loadCore("/morphscreen-agent.wasm");
const chat = connectAgent(document.querySelector("#chat")!, { core, url: "/api/agent" });
const form = document.querySelector("form")!;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const input = form.querySelector("input")!;
const text = input.value;
input.value = "";
await chat.send(text);
});
```
Serve `morphscreen-agent.wasm` from the package with your page. Each [integration](/docs/integrations) shows how for its build tool.
That's a working chat. Ask it for a trip, a dashboard or a quiz, and the answer arrives as cards, charts and questions, checked line by line.
## Where next
- [The format](/docs/format): what the model writes, and why half an answer is always valid.
- [Components](/docs/components): all 75, drawn live, with their attributes.
- [Your own components](/docs/format/custom-components): teach the model your product's own cards.
- [Actions and state](/docs/format/actions): what comes back when the reader taps, types and answers.
---
# MorphScreen vs others
> How MorphScreen differs from OpenUI, Thesys C1, A2UI and the Vercel AI SDK's generative UI, and where the measured numbers are.
Generative UI libraries share a goal: a model's answer that the reader can use, not just read. They differ in what the model writes, who checks it, and what happens when it's wrong.
## What MorphScreen does differently
- **The model writes Markdown with a few tags.** Models already write both, so the format needs a short prompt (about 1,400 tokens with the core components, 2,900 with every pack), and half an answer is always a valid start of the whole one. Nothing waits for a closing bracket or a root statement.
- **Every line is checked as it streams.** The core reads each line against the components your app allows, so what reaches your page is already validated, and a mistake is tied to the line it's on.
- **Only broken lines are repaired.** When a line breaks a rule, the API asks the same model to rewrite that line, not the whole answer.
- **Any model.** The model included in every plan, or any model on OpenRouter with your own key.
- **Nothing a model wrote ever runs.** No script, no style, no address you didn't allow (see [Security](/docs/production/security)).
## Against OpenUI, measured
We measure ourselves against OpenUI on the benchmark thesysdev publishes for it: the same 46 briefs, the same models and the same rules for what counts as valid. The [benchmark page](/benchmark) has the method, every raw answer and the rows where OpenUI is ahead with why; [every row](/compare/openui) is on its own page.
## The others
| Library | What the model writes | Measured against MorphScreen |
|---|---|---|
| [OpenUI](https://www.openui.com) | OpenUI Lang, a language of its own | yes: [the benchmark](/benchmark) |
| [A2UI](https://github.com/a2ui-project/a2ui) | JSON messages that describe components | not yet |
| [Vercel AI SDK](https://ai-sdk.dev) | tool calls, which your code turns into components | not yet |
| [Thesys C1](https://www.thesys.dev) | its own format, through its API | not yet |
Comparisons with the last three need runs of their own on the same briefs; they go on this page, with their raw answers, once we have them.
---
# Coding agent setup
> Point Claude Code, Cursor, Codex or any coding agent at MorphScreen's docs, written for agents to read.
Every page of these docs is also plain Markdown, and the whole set is one file. Give your coding agent either, and it can write MorphScreen code from the real reference instead of guessing.
## The docs, for agents
| What | Where |
|---|---|
| An index of every page, with links | [morphscreen.com/llms.txt](/llms.txt) |
| Every page in one file | [morphscreen.com/llms-full.txt](/llms-full.txt) |
| One page | its address with `.md` added, like [/docs/quickstart.md](/docs/quickstart.md) |
Every page also has **Copy Markdown** under its title, for pasting into a chat.
## A rule for your project
Add this to your agent's instructions file (`CLAUDE.md`, `AGENTS.md`, `.cursor/rules`):
```markdown
## MorphScreen
This app draws model answers with MorphScreen (@morphscreen/web).
- Docs: https://morphscreen.com/llms.txt (read the page you need as .md).
- Keep the MorphScreen key on the server; the page talks to our own route.
- Components and their attributes: https://morphscreen.com/docs/components.md
- Never render model text as HTML yourself; the SDK or /v1/render draws it.
```
## Asking for UI
The model your app uses is told about the components by the system prompt the API builds. Your coding agent doesn't need to know them to build the app, only to write [your own components](/docs/format/custom-components) or to check an answer by hand.
---
# The format
> What a model writes for MorphScreen: Markdown with a few components written like JSX, designed so half an answer is always a valid start of the whole one.
An answer is Markdown with components written like JSX. Models already write both, so the format needs little teaching, and the prompt that teaches it is short.
```markdown
## Weekend in Lisbon
Two days is enough for the old town and Belém, if you take the trams for the hills.
| Item | USD |
|---|---|
| Stay | 180 |
| Food | 90 |
| Trams | 60 |
- Where should I stay?
- What should I eat?
```
## Components
```markdown
content
```
- A component's name starts with a capital letter. `
` or ` ` is text.
- Values are quoted; numbers and `true` or `false` may go without quotes, and a bare attribute (`required`) means `true`.
- A list of values is split by ` | `: `options="S | M | L"`.
- A component with nothing inside closes itself, like ``.
- An attribute means the same on every component: `tone` is always `neutral`, `info`, `success`, `warning` or `danger`.
Each component says what goes between its tags: Markdown and other components, a Markdown table (a chart's data), a list (follow-up questions), images or nothing. The [component reference](/docs/components) has each one's attributes.
## Markdown
Text between components is Markdown: headings, paragraphs, emphasis, inline code, links, lists, quotes, code blocks, rules and tables. Raw HTML is never drawn as HTML. The few tags models slip in anyway, like `` and ` `, are read as formatting, and the rest shows as written.
## Why it streams well
- **There's no root to forget.** The answer is the root, and a component's place in the tree is where it's written.
- **A value counts only once it's whole.** A component appears when its opening tag is complete, so its attributes never change after it shows. A number or an address is never read half-written.
- **What's finished never changes.** A closed component is final, so the page never draws it twice.
[How streaming works](/docs/format/streaming) has the detail.
## What the core does with it
The core is the same Rust code everywhere: WebAssembly in the browser, inside the API on the server. As the text arrives it builds the tree, checks every line against the components your app allows, and reports each problem on its line. The API repairs those lines with the same model ([Validation and repair](/docs/format/validation)), and the SDK draws the tree.
The full grammar is the [format specification](/docs/reference/specification).
---
# Components and packs
> The 75 components come in a core that every answer has and eight packs an app can choose. Fewer packs, shorter prompt.
The components come in packs. `core` is always there: layout, text blocks, numbers, charts, forms and buttons. Eight more packs add data, commerce, the device, docs, inputs, interaction, media and scheduling.
| Pack | What it adds |
|---|---|
| [Core](/docs/components/core) | Cards, columns, tabs, steps, callouts, stats, badges, progress, charts, forms, buttons, follow-up questions |
| [Data](/docs/components/data) | Key-value facts, timelines, gauges, heatmaps, funnels, sparklines, trees |
| [Commerce](/docs/components/commerce) | Products, plans, carts, ratings, comparisons, prices |
| [Device](/docs/components/device) | Maps, camera, barcode scanning, location, files, contacts, calendar, sharing, voice notes, QR codes |
| [Docs](/docs/components/docs) | Code with a file name, diffs, formulas, file trees, citations |
| [Inputs](/docs/components/inputs) | Star ratings, searchable choices, times, toggle groups, steppers, signatures |
| [Interactive](/docs/components/interactive) | Quizzes, flashcards, polls, checklists, what-if sliders |
| [Media](/docs/components/media) | Video, audio, embeds, carousels |
| [Scheduling](/docs/components/scheduling) | Calendars, events, time slots, agendas, countdowns |
## Choosing packs
Every pack is on unless you name the ones you want. The model is told about only the components it may use, and the core checks the answer against the same list.
```js
{ "model": "deepseek/deepseek-v4.1-flash", "messages": [...], "morphscreen": { "packs": ["data", "interactive"] } }
```
```js
run.forwardedProps = { ...run.forwardedProps, model: MODEL, packs: ["data", "interactive"] };
```
```js
createAnswer(el, { core, packs: ["data", "interactive"] });
```
Give the page the same packs as the API, so the page can read everything the model was allowed to write. `[]` is the core alone.
## Prompt length
| Packs | System prompt |
|---|---|
| Core alone | about 1,400 tokens |
| Every pack | about 2,900 tokens |
Each component adds about 22 tokens. A shorter prompt costs less on every call and leaves the model fewer ways to go wrong; the benchmark measured both ([the benchmark](/benchmark)).
## Names models use on their own
Models sometimes write `
`, `` or `` without being told. The core reads them as the component they mean and notes it, rather than failing the answer.
---
# Your own components
> Teach the model your product's own components. Describe one once, give it to the API and the page, and say how to draw it.
Next to the built-in components, an app can add its own: a shop's product card, a booking's seat map, a support ticket. It takes three steps, and the same description serves all three: the model learns it from the prompt, the core checks answers against it, and your page draws it.
## Describe it
A component lives in a pack: a name, one line on what it's for, its attributes, and what goes between its tags.
```json
{
"name": "shop",
"version": "1.0.0",
"components": {
"ProductCard": {
"about": "a product with its price",
"attrs": {
"name": { "type": "string", "required": true },
"price": { "type": "string", "required": true },
"stock": ["in", "low", "out"]
},
"content": "none"
}
}
}
```
Attribute types are `string`, `number`, `boolean`, `url`, `list` (values split by ` | `), or a list of the values allowed, the first being the default. Content is `none`, `text`, `table`, `list`, `images` or `any` (Markdown and other components). One attribute, one meaning: `stock="low"`, not `style="color: orange"`.
A `text` component can take `"actions": true`. The model may then put `Button`s inside it, each with `ask` (the message a tap sends) or `href`, drawn under it as what the reader can do, like "Add to cart".
## Give it to the API
The API puts the pack's components in the system prompt and checks the answer against them. Send it from your server with every call:
```js
{ "model": "deepseek/deepseek-v4.1-flash", "messages": [...], "morphscreen": { "components": [shop] } }
```
```js
run.forwardedProps = { ...run.forwardedProps, model: MODEL, components: [shop] };
```
## Draw it
Give the page the same pack, and a function that draws the component into an element. It gets the component's attributes, and its content as HTML and as text; it may return a cleanup.
```ts
import { createAnswer } from "@morphscreen/web";
import shop from "./shop.json";
createAnswer(el, {
core,
custom: [shop],
components: {
ProductCard: (el, { attrs }) => {
el.textContent = `${attrs.name}: ${attrs.price}`;
},
},
});
```
With React, pass components:
```tsx
import { Answer, type CustomProps } from "@morphscreen/web/react";
import shop from "./shop.json";
function ProductCard({ attrs }: CustomProps) {
return (
{attrs.name} {attrs.price}
);
}
;
```
`connectAgent` takes `custom` and `components` the same way.
## Your styles, untouched
Your component is drawn in a `
` inside the answer. MorphScreen's stylesheet leaves everything in it to yours: it only inherits the answer's font and colour.
## When something's missing
A component your page doesn't draw still shows: its attributes as a list, then its content. Nothing an answer holds is dropped without a word.
## Names
A name defined twice is an error, never an override, so your pack can't replace a built-in component by accident. The one exception: when you leave `packs` out (every pack), your own `Product` or `Map` replaces the pack's. Name the pack outright and the two clash.
---
# The system prompt
> How the model learns the format. The API builds the prompt from your catalog and puts it before your own messages; you can also build it yourself.
The model learns the format from a system prompt built from the catalog: the components your app allows, their attributes, and the rules that keep answers valid. It's the same catalog the core checks answers against, so the two can't disagree.
## Through the API
The API builds the prompt for you. It goes first, and your own messages, system messages included, follow unchanged:
```json
{
"model": "deepseek/deepseek-v4.1-flash",
"messages": [
{ "role": "system", "content": "You are Lisbon Trams' trip planner. Answer in British English." },
{ "role": "user", "content": "Plan a weekend for two" }
],
"morphscreen": { "packs": ["data"] }
}
```
Your system message says who the assistant is and what it knows. Ours says how to answer. You don't need to describe the components yourself.
## Building it yourself
To run a model of your own, outside the API, build the same prompt with the core and send it as the first system message.
In the browser or Node, with the web SDK:
```ts
const prompt = core.prompt(["data"], [shop]); // packs, then your own packs
```
On the command line:
```bash
mscore prompt --packs data
```
Then check what comes back with the core yourself: a `Session` from `core.session(packs, custom)` reads the text as it streams and gives you the verdict ([the web SDK reference](/docs/reference/web)). The API does that, and the repair, for you.
## What it costs
With the core alone the prompt is about 1,400 tokens, and with every pack about 2,900: about 22 tokens a component. The prompt's version is a hash recorded with every call, so the dashboard and the benchmark can tell which prompt an answer came from.
---
# Validation and repair
> Every line is checked against your app's components as it streams, and the API asks the same model to rewrite only the lines that break a rule.
A model gets things wrong: an attribute that doesn't exist, a value outside its list, a component it made up. The core catches each one on its line, as the answer streams, and the API repairs only those lines.
## Lenient where the intent is clear
- **Values match without case, and a few synonyms count:** `error` means `danger`, `column` means `bar`. These are reported as notes, not errors.
- **A bad value is fixed at its edge:** the default if there is one, or an optional attribute is dropped. A bad table row drops only that row.
- **An unknown component is dropped** and its content kept as text, so nothing the model said is lost.
- **Names models use on their own** (`
", "morphscreen": { "valid": true, "truncated": false, "components": 1, "diagnostics": [] } }
```
Put it in a page with the stylesheets from the web SDK's package (`dist/morphscreen.css` and its `fonts/`, and `dist/morphscreen-page.css` for a page that is just the answer):
```html
```
Every component draws this way; the [component reference](/docs/components) is this HTML. `output: "ops"` or `"tree"` returns the core's op stream or tree instead, for drawing it yourself.
Renders are free up to ten times your plan's calls a month. Pictures load only from your images' and photo provider's hosts, and those you list in `image_hosts`.
---
# All components
> The 75 components a model can use, in 9 packs: what each is for, its attributes, and a live preview drawn by the core.
Every answer can use the `core` pack. The others are added by default; an app that names the packs it wants (`packs` in the API and the SDKs) gets core and those, and the model is told about only those. Fewer packs make a shorter system prompt: core alone is about 1,400 tokens, every pack about 2,900.
| Pack | Components | What it's for |
|---|---|---|
| [Core](/docs/components/core) | 25 | Always included: layout, text blocks, numbers, charts, forms and buttons. |
| [Data](/docs/components/data) | 7 | Ways to show data beyond charts and tables: facts, timelines, gauges, heatmaps, funnels, sparklines and hierarchies. |
| [Commerce](/docs/components/commerce) | 6 | Shopping: products, pricing plans, carts, ratings, comparisons and prices. |
| [Device](/docs/components/device) | 10 | The device's own hardware and apps: maps, camera, scanning, location, files, contacts, the calendar, sharing, voice notes and QR codes. Native on every platform, with a web fallback that needs no JavaScript. |
| [Docs](/docs/components/docs) | 5 | Technical writing: code with a file name, diffs, formulas, file trees and cited sources. |
| [Inputs](/docs/components/inputs) | 6 | More form fields: star ratings, searchable choices, times, toggle groups, steppers and signatures. |
| [Interactive](/docs/components/interactive) | 7 | Things the reader does on the page, kept on their device and reported back: a quiz, flashcards, a poll, a checklist, and what-if sliders. |
| [Media](/docs/components/media) | 4 | Video, audio, embeds from known hosts, and a carousel of images. |
| [Scheduling](/docs/components/scheduling) | 5 | Time: a month calendar, events, time slots to pick, a day's agenda and countdowns. |
## Names models use on their own
A few names models reach for without being told are read as the component they mean, and reported as a note, never taught:
| They write | It's read as |
|---|---|
| `
` | ``: a section holding a Markdown table |
| `` | a plain group: one cell of Columns |
| `` | a plain group: a row of buttons |
| `` | a plain group: what models call a run of text |
## Every component
- **Core:** [Card](/docs/components/core#card), [Columns](/docs/components/core#columns), [Tabs](/docs/components/core#tabs), [Tab](/docs/components/core#tab), [Details](/docs/components/core#details), [Steps](/docs/components/core#steps), [Step](/docs/components/core#step), [Callout](/docs/components/core#callout), [Stat](/docs/components/core#stat), [Badge](/docs/components/core#badge), [Progress](/docs/components/core#progress), [Avatar](/docs/components/core#avatar), [Gallery](/docs/components/core#gallery), [Chart](/docs/components/core#chart), [FollowUps](/docs/components/core#followups), [Form](/docs/components/core#form), [Input](/docs/components/core#input), [TextArea](/docs/components/core#textarea), [Select](/docs/components/core#select), [Date](/docs/components/core#date), [Slider](/docs/components/core#slider), [Checkboxes](/docs/components/core#checkboxes), [Radio](/docs/components/core#radio), [Switch](/docs/components/core#switch), [Button](/docs/components/core#button)
- **Data:** [KeyValue](/docs/components/data#keyvalue), [Timeline](/docs/components/data#timeline), [Gauge](/docs/components/data#gauge), [Heatmap](/docs/components/data#heatmap), [Funnel](/docs/components/data#funnel), [Sparkline](/docs/components/data#sparkline), [Tree](/docs/components/data#tree)
- **Commerce:** [Product](/docs/components/commerce#product), [Plan](/docs/components/commerce#plan), [Cart](/docs/components/commerce#cart), [Rating](/docs/components/commerce#rating), [Compare](/docs/components/commerce#compare), [Price](/docs/components/commerce#price)
- **Device:** [Map](/docs/components/device#map), [Camera](/docs/components/device#camera), [Scan](/docs/components/device#scan), [Location](/docs/components/device#location), [File](/docs/components/device#file), [Contact](/docs/components/device#contact), [AddToCalendar](/docs/components/device#addtocalendar), [Share](/docs/components/device#share), [VoiceNote](/docs/components/device#voicenote), [QR](/docs/components/device#qr)
- **Docs:** [Code](/docs/components/docs#code), [Diff](/docs/components/docs#diff), [Math](/docs/components/docs#math), [FileTree](/docs/components/docs#filetree), [Citation](/docs/components/docs#citation)
- **Inputs:** [RatingInput](/docs/components/inputs#ratinginput), [Combobox](/docs/components/inputs#combobox), [Time](/docs/components/inputs#time), [ToggleGroup](/docs/components/inputs#togglegroup), [Stepper](/docs/components/inputs#stepper), [Signature](/docs/components/inputs#signature)
- **Interactive:** [Quiz](/docs/components/interactive#quiz), [Question](/docs/components/interactive#question), [Flashcards](/docs/components/interactive#flashcards), [Poll](/docs/components/interactive#poll), [Checklist](/docs/components/interactive#checklist), [WhatIf](/docs/components/interactive#whatif), [Result](/docs/components/interactive#result)
- **Media:** [Video](/docs/components/media#video), [Audio](/docs/components/media#audio), [Embed](/docs/components/media#embed), [Carousel](/docs/components/media#carousel)
- **Scheduling:** [Calendar](/docs/components/scheduling#calendar), [Event](/docs/components/scheduling#event), [TimeSlots](/docs/components/scheduling#timeslots), [Agenda](/docs/components/scheduling#agenda), [Countdown](/docs/components/scheduling#countdown)
---
# Core components
> The core pack's 25 components: always included: layout, text blocks, numbers, charts, forms and buttons.
Core comes with every answer, whichever packs an app names.
## Markdown
Text between components is Markdown: headings, lists, tables, emphasis, links, code and images from the hosts an app allows.
```markdown
## A weekend in Lisbon
Lisbon is **compact and hilly**. Most of what you want to see sits between Baixa and Belém, and the trams cover the steep parts.

- Day one: Alfama, the castle, a fado dinner
- Day two: Belém, pastries, the river walk
- Day three: Sintra by train (40 minutes from Rossio)
| Neighbourhood | Good for | From the centre |
|---|---|---|
| Alfama | Old streets, fado | 10 min walk |
| Belém | Monuments, pastries | 20 min by tram |
| Príncipe Real | Gardens, shops | 15 min walk |
> The 28 tram is lovely but packed after 10:00. Ride it early.
Book the castle online with `code LISBOA10` for 10% off.
```
## Card
A section, optionally with a header.
Between its tags: Markdown and other components.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
| `subtitle` | text | | |
```markdown
Flights are booked. What's left is the hotel, food and two day trips, about **€740** in all.
```
## Columns
Puts its children side by side, stacking on narrow screens.
Between its tags: Markdown and other components.
```markdown
Three nights in Chiado, breakfast included. Check-in from 15:00.
TP 1351 out on Friday 08:40, TP 1358 back on Monday 19:15.
A Viva Viagem card covers trams, metro and the river ferries.
```
## Tabs
One panel per Tab.
Between its tags: only `Tab` components.
```markdown
Three nights, flights booked, hotel to confirm by Thursday.
- Time Out Market for the first night
- Pastéis de Belém on day two
- A tasca in Alfama for fado
Comfortable shoes (the hills are steep), a light jacket for the evenings, sunscreen.
```
## Tab
A panel inside Tabs.
Between its tags: Markdown and other components. It goes inside `Tabs`.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | yes | |
```markdown
Three nights, flights booked, hotel to confirm by Thursday.
- Time Out Market for the first night
- Pastéis de Belém on day two
- A tasca in Alfama for fado
Comfortable shoes (the hills are steep), a light jacket for the evenings, sunscreen.
```
## Details
A collapsible section, closed until opened (open starts it open).
Between its tags: Markdown and other components.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | yes | |
| `open` | true or false | | |
```markdown
Weekend rates in Chiado run about 30% higher, and this weekend there's a football match at the Estádio da Luz.
Free cancellation until 48 hours before check-in. After that the first night is charged.
```
## Steps
An ordered sequence or timeline.
Between its tags: only `Step` components.
```markdown
Reply to the hotel's email with the arrival time.
```
## Step
One step inside Steps.
Between its tags: Markdown and other components. It goes inside `Steps`.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | yes | |
| `status` | one of | | `done`, `current`, `todo` (default) |
| `subtitle` | text | | |
```markdown
Reply to the hotel's email with the arrival time.
```
## Callout
A highlighted note or alert.
Between its tags: Markdown and other components.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `tone` | one of | | `neutral`, `info` (default), `success`, `warning`, `danger` |
| `title` | text | | |
```markdown
Museums are free on Sunday mornings for residents only, so check the ticket rules.Your table at Taberna Sal Grosso is confirmed for 20:30.Metro staff plan a partial strike from 06:30 to 10:30.It must be valid for three months after your return date.Prices are in euros and include VAT.
```
## Stat
One key number, its unit inside value ("3.7 km", "$1,204").
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `value` | text | yes | |
| `change` | text | | |
| `note` | text | | |
| `tone` | one of | | `neutral` (default), `info`, `success`, `warning`, `danger` |
| `icon` | icon name | | |
```markdown
```
## Badge
A short status label.
Between its tags: inline text: its label.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `tone` | one of | | `neutral` (default), `info`, `success`, `warning`, `danger` |
```markdown
Order **#4821** is Shipped, invoice Overdue, return window 14 days, payment Failed, label Draft.
```
## Progress
A progress bar.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `value` | number | yes | |
| `max` | number | | default `100` |
| `label` | text | | |
| `tone` | one of | | `neutral` (default), `info`, `success`, `warning`, `danger` |
```markdown
```
## Avatar
A person's picture or initials, with their name.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `name` | text | yes | |
| `src` | address | | |
| `subtitle` | text | | |
```markdown
```
## Gallery
A grid of images.
Between its tags: Markdown images, one a line.
```markdown



```
## Chart
Charts the Markdown table inside: first column labels, every column after it numbers (one series each).
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `type` | one of | | `bar` (default), `line`, `area`, `pie`, `donut`, `radar`, `scatter`, `stacked`, `horizontal` |
| `title` | text | | |
| `x` | text | | |
| `y` | text | | |
### bar
```markdown
| Month | Signups |
|---|---|
| Jan | 120 |
| Feb | 180 |
| Mar | 240 |
| Apr | 210 |
| May | 305 |
```
### line
```markdown
| Round | Antonelli | Russell | Hamilton |
|---|---|---|---|
| 1 | 25 | 18 | 15 |
| 2 | 43 | 36 | 27 |
| 3 | 68 | 51 | 45 |
| 4 | 93 | 69 | 57 |
| 5 | 111 | 94 | 70 |
```
### area
```markdown
| Day | Visitors |
|---|---|
| Mon | 1,200 |
| Tue | 1,450 |
| Wed | 1,380 |
| Thu | 1,720 |
| Fri | 1,960 |
```
### stacked
```markdown
| Week | High | Medium | Low |
|---|---|---|---|
| W1 | 12 | 22 | 30 |
| W2 | 9 | 25 | 34 |
| W3 | 14 | 19 | 28 |
| W4 | 7 | 21 | 31 |
```
### horizontal
```markdown
| Course | Fill % |
|---|---|
| CS 101 Intro to Programming | 98 |
| BIO 150 Genetics | 89 |
| ART 210 Modern Art History | 78 |
| ECO 120 Microeconomics | 71 |
```
### scatter
```markdown
| Price | Rating |
|---|---|
| 49 | 4.1 |
| 79 | 3.6 |
| 129 | 4.8 |
| 199 | 4.4 |
| 249 | 4.6 |
```
### pie
```markdown
| Source | Visits |
|---|---|
| Search | 5,400 |
| Social | 2,100 |
| Direct | 1,500 |
| Email | 600 |
```
### donut
```markdown
| Plan | Customers |
|---|---|
| Free | 1,842 |
| Pro | 1,214 |
| Team | 702 |
```
### radar
```markdown
| Skill | Alex | Sam |
|---|---|---|
| Speed | 8 | 6 |
| Power | 6 | 9 |
| Accuracy | 9 | 7 |
| Stamina | 7 | 8 |
| Teamwork | 8 | 7 |
```
## FollowUps
Suggested next questions the user can tap.
Between its tags: a Markdown list.
```markdown
- What's the cheapest way to get to Sintra?
- Find me a fado house that isn't touristy
- Plan day two around the rain forecast
```
## Form
Groups fields; its Button submits them.
Between its tags: Markdown and other components.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `name` | text | | |
```markdown
```
## Input
A one-line text field.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `type` | one of | | `text` (default), `email`, `number`, `password`, `url`, `tel`, `search`, `date`, `time`, `datetime-local` |
| `placeholder` | text | | |
| `value` | text | | |
| `step` | number | | |
| `min` | number | | |
| `max` | number | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## TextArea
A multi-line text field.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `placeholder` | text | | |
| `value` | text | | |
| `rows` | number | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Select
A dropdown.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `options` | values split by ` | ` | yes | |
| `name` | text | | |
| `placeholder` | text | | |
| `value` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Date
A date picker.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `value` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Slider
Pick a number in a range, shown with its unit ($, %, km).
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `min` | number | yes | |
| `max` | number | yes | |
| `step` | number | | |
| `value` | number | | |
| `name` | text | | |
| `unit` | text | | |
```markdown
```
## Checkboxes
Pick any number of options.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `options` | values split by ` | ` | yes | |
| `name` | text | | |
| `value` | values split by ` | ` | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Radio
Pick one option.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `options` | values split by ` | ` | yes | |
| `name` | text | | |
| `value` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Switch
An on/off toggle.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `value` | true or false | | |
| `hint` | text | | |
```markdown
```
## Button
In a Form it submits; elsewhere href opens a link and ask sends a message (the label by default); the label goes between the tags or in label.
Between its tags: inline text: its label.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `variant` | one of | | `primary` (default), `secondary`, `tertiary`, `danger` |
| `href` | address | | |
| `ask` | text | | |
| `disabled` | true or false | | |
| `size` | one of | | `medium` (default), `small`, `large` |
| `label` | text | | |
| `icon` | icon name | | |
```markdown
```
---
# Data components
> The data pack's 7 components: ways to show data beyond charts and tables: facts, timelines, gauges, heatmaps, funnels, sparklines and hierarchies.
Add it with `packs: ["data"]`, or leave `packs` out for every pack.
## KeyValue
Labelled facts, from the Markdown table inside: the label, then the value.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
| Field | Value |
|---|---|
| Name | api-eu-1 |
| Region | eu-west-1 |
| Uptime | 41 days |
| Version | 2.14.3 |
```
## Timeline
Events in order, one per list item, written as "when: what happened".
Between its tags: a Markdown list.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
- 2019: Founded in a Lisbon flat
- 2021: First 1,000 customers
- 2023: Opened the Berlin office
- 2024: Series B, 120 people
```
## Gauge
One number on a scale from min to max.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `value` | number | yes | |
| `min` | number | | |
| `max` | number | | |
| `unit` | text | | |
| `tone` | one of | | `neutral` (default), `info`, `success`, `warning`, `danger` |
```markdown
```
## Heatmap
A grid of numbers shaded by size, from the Markdown table inside: the row label, then one column per bucket.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
| Day | Morning | Afternoon | Evening |
|---|---|---|---|
| Mon | 4 | 12 | 3 |
| Tue | 8 | 15 | 1 |
| Wed | 2 | 9 | 7 |
| Thu | 6 | 18 | 5 |
| Fri | 3 | 7 | 0 |
```
## Funnel
How many reach each stage, from the Markdown table inside: the stage, then the count.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
| Step | Users |
|---|---|
| Landing | 12,000 |
| Signup | 3,100 |
| Verified | 2,400 |
| Paid | 410 |
```
## Sparkline
A number with a small trend line from its recent values.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `values` | values split by ` | ` | yes | |
| `value` | text | | |
| `change` | text | | |
```markdown
```
## Tree
A hierarchy, one list item per entry, written as its path with " > " between levels.
Between its tags: a Markdown list.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
- Engineering > Platform
- Engineering > Mobile
- Engineering > Mobile > iOS
- Sales > EMEA
- Sales > Americas > US
- Sales > Americas > Brazil
```
---
# Commerce components
> The commerce pack's 6 components: shopping: products, pricing plans, carts, ratings, comparisons and prices.
Add it with `packs: ["commerce"]`, or leave `packs` out for every pack.
## Product
One product with its price; a short description between the tags, then a Button with ask for each thing the reader can do with it (Add to cart, Tell me more); rating is out of 5, count how many rated it. When the reader is choosing what to buy, show each option as one, side by side in Columns.
Between its tags: inline text: its label. It also takes `Button`s, drawn under it as what the reader can do.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `name` | text | yes | |
| `price` | text | | |
| `was` | text | | |
| `image` | address | | |
| `rating` | number | | |
| `count` | number | | |
| `badge` | text | | |
| `href` | address | | |
```markdown
Closed-back, 30 hours of battery, folds flat.Small case, noise cancelling, 8 hours a charge.
```
## Plan
A pricing plan, its features as a Markdown list inside.
Between its tags: a Markdown list.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `name` | text | yes | |
| `price` | text | yes | |
| `period` | text | | |
| `note` | text | | |
| `featured` | true or false | | |
```markdown
- 3 projects
- Community support
- Unlimited projects
- SSO
- Priority support
- Audit logs
- 99.99% SLA
- A named contact
```
## Cart
Items and totals, from the Markdown table inside: the item, the quantity, the price.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
| `total` | text | | |
| `note` | text | | |
```markdown
| Item | Qty | Price |
|---|---|---|
| Studio Headphones | 1 | $249.00 |
| USB-C cable | 2 | $24.00 |
| Hard case | 1 | $139.00 |
```
## Rating
A real star rating out of max, never a made-up one.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `value` | number | yes | |
| `max` | number | | |
| `count` | text | | |
```markdown
The Studio Headphones are rated by buyers, the earbuds .
```
## Compare
Options side by side, from the Markdown table inside: the feature, then one column per option; yes and no become marks.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
| Feature | Pixel 10 | iPhone 17 | Galaxy S26 |
|---|---|---|---|
| Screen | 6.3" | 6.1" | 6.2" |
| Battery | 4,700 mAh | 3,600 mAh | 4,000 mAh |
| 5G | yes | yes | yes |
| Headphone jack | no | no | no |
| Price | $799 | $829 | $799 |
```
## Price
A price, with the old one struck through.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `value` | text | yes | |
| `was` | text | | |
| `period` | text | | |
| `note` | text | | |
```markdown
```
---
# Device components
> The device pack's 10 components: the device's own hardware and apps: maps, camera, scanning, location, files, contacts, the calendar, sharing, voice notes and QR codes. Native on every platform, with a web fallback that needs no JavaScript.
Add it with `packs: ["device"]`, or leave `packs` out for every pack.
## Map
Places on a map; add one whenever the answer suggests places to go (a trip, places to eat, walks), even when not asked for a map. A Markdown table inside: the place first, then its Lat and Lng (an Address only for a place you can't locate), then anything else; route draws a path through them in order.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
| `route` | true or false | | |
```markdown
```
## Camera
A form field that takes a photo with the camera.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `facing` | one of | | `back` (default), `front` |
| `multiple` | true or false | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Scan
A form field filled by scanning a barcode or QR code.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `kind` | one of | | `any` (default), `qr`, `barcode` |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Location
A form field holding the user's location once they choose to share it.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `precision` | one of | | `approximate` (default), `precise` |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## File
A form field for attaching files.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `accept` | values split by ` | ` | | |
| `multiple` | true or false | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Contact
A form field: a person picked from the user's contacts.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## AddToCalendar
A button that adds an event to the user's calendar; the button's label between the tags.
Between its tags: inline text: its label.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | yes | |
| `start` | text | yes | |
| `end` | text | | |
| `where` | text | | |
| `note` | text | | |
```markdown
Add to calendar
```
## Share
A button that opens the share sheet with this text or link; the button's label between the tags.
Between its tags: inline text: its label.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `text` | text | | |
| `href` | address | | |
```markdown
Share the plan
```
## VoiceNote
A form field: the user records a short voice note.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `max` | number | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## QR
A QR code for a link or short text, for another phone to scan.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `value` | text | yes | |
| `label` | text | | |
```markdown
```
---
# Docs components
> The docs pack's 5 components: technical writing: code with a file name, diffs, formulas, file trees and cited sources.
Add it with `packs: ["docs"]`, or leave `packs` out for every pack.
## Code
A fenced code block inside, shown with its file name and highlighted lines.
Between its tags: code, as written.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `file` | text | | |
| `highlight` | values split by ` | ` | | |
```markdown
```toml
[server]
port = 8080
host = "0.0.0.0"
workers = 4
```
```
## Diff
A change, from the fenced diff block inside (lines starting with + and -).
Between its tags: code, as written.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `file` | text | | |
```markdown
```diff
@@ -1,4 +1,4 @@
-const limit = 10;
+const limit = 100;
const window = 60;
export { limit, window };
```
```
## Math
A formula written in TeX.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `tex` | text | yes | |
| `label` | text | | |
```markdown
```
## FileTree
Files and folders, one list item per path, folders ending in /.
Between its tags: a Markdown list.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
- src/
- src/main.rs
- src/lib.rs
- src/parse/
- src/parse/mod.rs
- Cargo.toml
- README.md
```
## Citation
A source the answer relies on; what it says between the tags.
Between its tags: inline text: its label.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | yes | |
| `href` | address | | |
| `source` | text | | |
| `date` | text | | |
```markdown
Caffeine six hours before bed cut total sleep by more than an hour.
```
---
# Inputs components
> The inputs pack's 6 components: more form fields: star ratings, searchable choices, times, toggle groups, steppers and signatures.
Add it with `packs: ["inputs"]`, or leave `packs` out for every pack.
## RatingInput
A form field: a star rating out of max.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `max` | number | | |
| `value` | number | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Combobox
A form field: type to filter the options, or type something new.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `options` | values split by ` | ` | yes | |
| `name` | text | | |
| `placeholder` | text | | |
| `value` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Time
A form field: a time of day as HH:MM.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `value` | text | | |
| `min` | text | | |
| `max` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## ToggleGroup
A form field: options as a row of toggles, one or several chosen.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `options` | values split by ` | ` | yes | |
| `name` | text | | |
| `value` | values split by ` | ` | | |
| `multiple` | true or false | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Stepper
A form field: a whole number with minus and plus.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `value` | number | | |
| `min` | number | | |
| `max` | number | | |
| `step` | number | | |
| `unit` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Signature
A form field: the user signs with a finger or the mouse.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `name` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
---
# Interactive components
> The interactive pack's 7 components: things the reader does on the page, kept on their device and reported back: a quiz, flashcards, a poll, a checklist, and what-if sliders.
Add it with `packs: ["interactive"]`, or leave `packs` out for every pack.
## Quiz
Questions with one right answer each: the reader picks, sees right or wrong and why, then a score. End with one when the reader is learning something.
Between its tags: only `Question` components.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
- A. Stomata
- B. Chlorophyll
- C. Xylem
- Oxygen
- Carbon dioxide
- Nitrogen
```
## Question
One question inside Quiz; its choices are the Markdown list inside; answer, always given, names the right one (its letter or words).
Between its tags: a Markdown list. It goes inside `Quiz`.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `text` | text | yes | |
| `answer` | text | yes | |
| `explain` | text | | |
```markdown
- A. Stomata
- B. Chlorophyll
- C. Xylem
- Oxygen
- Carbon dioxide
- Nitrogen
```
## Flashcards
Cards the reader flips, from the Markdown table inside: the front, then the back. For words, terms or facts to remember.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
| Term | Meaning |
|---|---|
| Chlorophyll | The green pigment that absorbs light |
| Stomata | Pores that let gases in and out |
| Glucose | The sugar a plant makes |
| Xylem | Tubes that carry water up from the roots |
```
## Poll
A vote with one choice.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `options` | values split by ` | ` | yes | |
| `name` | text | | |
```markdown
```
## Checklist
Tasks to tick off, one per Markdown list item ([x] marks one done), with progress. For things the reader has to do, pack or buy.
Between its tags: a Markdown list.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
| `name` | text | | |
```markdown
- [x] Read chapter 4
- [ ] Draw the leaf diagram
- [ ] Do the 2024 past paper
- [ ] Make flashcards for the key terms
```
## WhatIf
Sliders and the Results they change, worked out as the reader moves them. For an answer that turns on numbers the reader could change.
Between its tags: Markdown and other components.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
```
## Result
A number inside WhatIf; value is a formula of the Sliders' names (+ - * / ^, min, max, round).
Between its tags: nothing: it closes itself (``). It goes inside `WhatIf`.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `value` | text | yes | |
| `unit` | text | | |
| `decimals` | number | | |
```markdown
```
---
# Media components
> The media pack's 4 components: video, audio, embeds from known hosts, and a carousel of images.
Add it with `packs: ["media"]`, or leave `packs` out for every pack.
## Video
A video file to play.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `src` | address | yes | |
| `poster` | address | | |
| `title` | text | | |
| `caption` | text | | |
```markdown
```
## Audio
An audio file to play.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `src` | address | yes | |
| `title` | text | | |
```markdown
```
## Embed
A YouTube, Vimeo, Spotify or SoundCloud page shown in place; other pages become a link.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `src` | address | yes | |
| `title` | text | | |
```markdown
```
## Carousel
Images one at a time, from the Markdown images inside. For a set of things to look through: dishes, rooms, products, sights.
Between its tags: Markdown images, one a line.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown



```
---
# Scheduling components
> The scheduling pack's 5 components: time: a month calendar, events, time slots to pick, a day's agenda and countdowns.
Add it with `packs: ["scheduling"]`, or leave `packs` out for every pack.
## Calendar
A month with marked days, from the Markdown table inside: the date as YYYY-MM-DD, then what's on.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `month` | text | | |
| `title` | text | | |
```markdown
| Date | What |
|---|---|
| 2026-10-06 | Sprint planning |
| 2026-10-15 | Design review |
| 2026-10-20 | Launch |
| 2026-10-31 | Quarter closes |
```
## Event
One event; details between the tags.
Between its tags: inline text: its label.
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | yes | |
| `start` | text | yes | |
| `end` | text | | |
| `where` | text | | |
| `tone` | one of | | `neutral` (default), `info`, `success`, `warning`, `danger` |
```markdown
Bring a guest. Drinks and food from 18:30.
```
## TimeSlots
A form field: pick one of the times.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `options` | values split by ` | ` | yes | |
| `name` | text | | |
| `value` | text | | |
| `hint` | text | | |
| `required` | true or false | | |
```markdown
```
## Agenda
A day in time order, from the Markdown table inside: the time, then what.
Between its tags: a Markdown table (first column labels, the others series).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `title` | text | | |
```markdown
| Time | Talk |
|---|---|
| 9:00-9:45am | Keynote: the next ten years of the web |
| 10:30-11:15am | Rust on phones |
| 12:30–1:00pm | Lunch |
| 1:30-2:15pm | Designing for streaming UIs |
```
## Countdown
Time left until a moment, given as YYYY-MM-DDTHH:MM.
Between its tags: nothing: it closes itself (``).
| Attribute | Type | Required | Values |
|---|---|---|---|
| `label` | text | yes | |
| `until` | text | yes | |
```markdown
```
---
# The HTTP API
> The base address, the two keys, the four endpoints you call, and where the API's machine-readable description lives.
```text
https://api.morphscreen.com/v1
```
## Keys
| Header | What it is |
|---|---|
| `X-MorphScreen-Key: ms_live_...` | Your MorphScreen key, on every call. The call counts against your plan. Keep it on your server. |
| `Authorization: Bearer sk-or-...` | Your own OpenRouter key, when you want a model other than the one in your plan. It goes to OpenRouter and nowhere else, never stored or logged. Leave it out to use the model in your plan. |
## Endpoints
| Endpoint | What it does |
|---|---|
| [`POST /v1/chat/completions`](/docs/reference/api/chat-completions) | OpenAI's chat completion, answered in our format, checked and repaired; streamed or whole |
| [`POST /v1/ui`](/docs/reference/api/ui) | An AG-UI run, what the web SDK's `connectAgent` reads |
| [`POST /v1/render`](/docs/reference/api/render) | An answer you already have, drawn as HTML (or ops, or the tree) with no model called |
| [`GET /v1/health`](/docs/reference/api/health) | Whether the API is up |
## Options
Each model call takes the same options: in a `morphscreen` field for chat completions, in `forwardedProps` for AG-UI, at the top level for render.
| Option | What it does |
|---|---|
| `packs` | the packs to add to core; every pack when left out, `[]` for core alone |
| `components` | your own packs ([Your own components](/docs/format/custom-components)) |
| `images` | your own pictures, which the model shows by id |
| `photos` | your own Pexels, Unsplash or Openverse key, for photos the model asks for |
| `image_hosts` | more hosts the HTML's pictures may load from |
| `repair` | repair an answer with errors (default `true`) |
| `output` | what comes back: `text` or `html` for chat completions, `agui` or `ops` for AG-UI, `html`, `ops` or `tree` for render |
## The description
The API's OpenAPI 3.1 description is at [`/v1/openapi.json`](https://api.morphscreen.com/v1/openapi.json). Our tests hold the server to it, and these reference pages are generated from it.
[Limits and errors](/docs/production/limits) has every error and what to do about it.
---
# POST /v1/chat/completions
> A chat completion in our format, OpenAI's and OpenRouter's request and response.
## Keys
Your MorphScreen key in `X-MorphScreen-Key`: the call counts against your plan. Your own OpenRouter key in `Authorization: Bearer sk-or-...` when you want a model other than the one in your plan: it runs on that key, and goes to OpenRouter and nowhere else. Leave it out to use the model in your plan.
## Example
Stream an answer on the model included in your plan:
```bash
curl https://api.morphscreen.com/v1/chat/completions \
-H "X-MorphScreen-Key: $MORPHSCREEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "deepseek/deepseek-v4.1-flash", "stream": true,
"messages": [{"role": "user", "content": "Plan a weekend in Lisbon"}],
"morphscreen": {"packs": ["data"]}}'
```
Any other model on OpenRouter runs on your own key, sent as `Authorization: Bearer sk-or-...`. The stream is OpenAI's: `chat.completion.chunk` events whose `delta.content` is the answer in our format, then a last chunk with `finish_reason` and a `morphscreen` field holding the core's verdict (and the repaired text, when a repair was needed), then `data: [DONE]`.
## Request
### ChatCompletionRequest
OpenAI's chat completion request. Every field not listed here (temperature, max_tokens, models, provider, reasoning...) goes to the model as you set it. DeepSeek V4 Flash answers with reasoning off unless you set `reasoning`.
| Field | Type | Required | What it is |
|---|---|---|---|
| `model` | string | yes | An OpenRouter model name, like deepseek/deepseek-v4.1-flash. |
| `messages` | list of object | yes | |
| `stream` | boolean | | Default `false`. |
| `stream_options` | object | | |
| `n` | `1` | | |
| `morphscreen` | object | | output: text (the answer in our format, the default) or html (drawn by the core; not streamed). |
### Message
A chat message, as OpenAI and OpenRouter take it.
| Field | Type | Required | What it is |
|---|---|---|---|
| `role` | `system`, `developer`, `user`, `assistant`, `tool` | yes | |
| `content` | string or array or null | | |
### stream_options
| Field | Type | Required | What it is |
|---|---|---|---|
| `include_usage` | boolean | | |
### Options
output: text (the answer in our format, the default) or html (drawn by the core; not streamed).
| Field | Type | Required | What it is |
|---|---|---|---|
| `packs` | list of string | | The core's packs to add: data, commerce, scheduling, media, docs, inputs, device, interactive, or all. Left out, every pack ("all"); [] for the core alone, a prompt about half as long. At most 16. |
| `components` | list of object | | Your own packs, in the shape of spec/packs/*.json; 256 KB at most. At most 16. |
| `image_hosts` | list of string | | Hosts the HTML's pictures may load from, besides the app's images' hosts and its photo provider's. At most 16. |
| `images` | list of object | | Your own pictures, which the model shows by id. At most 200. |
| `photos` | one of the shapes below | | The app's own photo key, for one provider: with it the model may ask for photos, searched there on the app's quota, each loading from the provider's own address with its credit. Without one (or false) the model isn't asked for photos. |
| `output` | string | | What comes back; each endpoint has its own choices. |
| `repair` | boolean | | Repair an answer with errors, and give a blank one a fresh try. Default `true`. |
### AppImage
One of your own pictures. The model shows it as  and the API puts its URL and credit in; its host is allowed with it.
| Field | Type | Required | What it is |
|---|---|---|---|
| `id` | string | yes | Letters, digits, '-', '_' or '.'. |
| `url` | string | yes | Where it loads from, http(s). |
| `alt` | string | | What it shows, told to the model. |
| `credit` | string | | Shown under it, as written (Markdown links allowed). |
## Responses
| Status | What comes back |
|---|---|
| 200 | The answer whole, or with stream true as server-sent events: chat.completion.chunk after chunk, the last with finish_reason and morphscreen (and the repaired text when a repair was needed), a usage chunk when asked, then `data: [DONE]`. |
| 400 | The request can't be run as written; model_needs_your_key: a model other than the included one, asked for without your own OpenRouter key. |
| 401 | No key, or one turned down: your MorphScreen key (morphscreen_key_required, morphscreen_key_invalid) or your OpenRouter key (openrouter_key_required, openrouter_key_invalid). |
| 402 | This month's calls on your plan are used (plan_quota_reached), a paid plan's month and as many again as extra (extra_limit_reached), the free plan's calls with the model included (included_calls_used, or included_calls_paused while our credit is low), or the OpenRouter account is out of credit (openrouter_out_of_credit). |
| 403 | The account is suspended (account_suspended), or the model's moderation refused the request (model_refused). |
| 404 | No such model. |
| 413 | The body is over 1 MB (payload_too_large), or a call with the model included has more than about 8,000 tokens of conversation (conversation_too_long). |
| 429 | A limit reached, with Retry-After saying how long to wait: requests a minute (rate_limited), answers at once (too_many_streams), the free tier's day (daily_limit_reached), renders this month (render_limit_reached), or OpenRouter's own (openrouter_rate_limited). |
| 502 | The model failed or couldn't be reached, after the retries. |
| 503 | Accounts can't be read just now; try again. |
| 504 | The model took too long to start. |
Errors come back as `{"error": {"code", "message", "type", "metadata": {"error_type", "reason"}}}`: `type` is the reason to act on (`rate_limited`, `plan_quota_reached`...). [Limits and errors](/docs/production/limits) says what to do about each.
---
# POST /v1/ui
> An AG-UI run (protocol 1.0), what the MorphScreen SDKs read.
## Keys
Your MorphScreen key in `X-MorphScreen-Key`: the call counts against your plan. Your own OpenRouter key in `Authorization: Bearer sk-or-...` when you want a model other than the one in your plan: it runs on that key, and goes to OpenRouter and nowhere else. Leave it out to use the model in your plan.
## Example
An AG-UI run, which `connectAgent` sends for you; the model and the options go in `forwardedProps`:
```bash
curl -N https://api.morphscreen.com/v1/ui \
-H "X-MorphScreen-Key: $MORPHSCREEN_KEY" \
-H "Content-Type: application/json" \
-d '{"threadId": "t1", "runId": "r1", "tools": [], "context": [], "state": {},
"messages": [{"id": "m1", "role": "user", "content": "Quiz me on photosynthesis"}],
"forwardedProps": {"model": "deepseek/deepseek-v4.1-flash", "packs": ["interactive"]}}'
```
## Request
### RunAgentInput
AG-UI's RunAgentInput (protocol 1.0). Tools aren't supported yet: `tools` must be empty.
| Field | Type | Required | What it is |
|---|---|---|---|
| `threadId` | string | | |
| `runId` | string | | |
| `messages` | list of object | | |
| `tools` | list of any | | At most 0. |
| `state` | any | | |
| `context` | list of any | | |
| `forwardedProps` | object | yes | The model and the options. output: agui (AG-UI events, the default) or ops (the core's op stream). |
### forwardedProps
The model and the options. output: agui (AG-UI events, the default) or ops (the core's op stream).
| Field | Type | Required | What it is |
|---|---|---|---|
| `model` | string | yes | |
| `packs` | list of string | | |
| `components` | list of object | | |
| `image_hosts` | list of string | | |
| `images` | list of object | | At most 200. |
| `photos` | one of the shapes below | | The app's own photo key, for one provider: with it the model may ask for photos, searched there on the app's quota, each loading from the provider's own address with its credit. Without one (or false) the model isn't asked for photos. |
| `output` | `agui`, `ops` | | |
| `repair` | boolean | | |
### AppImage
One of your own pictures. The model shows it as  and the API puts its URL and credit in; its host is allowed with it.
| Field | Type | Required | What it is |
|---|---|---|---|
| `id` | string | yes | Letters, digits, '-', '_' or '.'. |
| `url` | string | yes | Where it loads from, http(s). |
| `alt` | string | | What it shows, told to the model. |
| `credit` | string | | Shown under it, as written (Markdown links allowed). |
## Responses
| Status | What comes back |
|---|---|
| 200 | Server-sent events: AG-UI events (output agui), or the core's ops (output ops). |
| 400 | The request can't be run as written; model_needs_your_key: a model other than the included one, asked for without your own OpenRouter key. |
| 401 | No key, or one turned down: your MorphScreen key (morphscreen_key_required, morphscreen_key_invalid) or your OpenRouter key (openrouter_key_required, openrouter_key_invalid). |
| 402 | This month's calls on your plan are used (plan_quota_reached), a paid plan's month and as many again as extra (extra_limit_reached), the free plan's calls with the model included (included_calls_used, or included_calls_paused while our credit is low), or the OpenRouter account is out of credit (openrouter_out_of_credit). |
| 403 | The account is suspended (account_suspended), or the model's moderation refused the request (model_refused). |
| 404 | No such model. |
| 413 | The body is over 1 MB (payload_too_large), or a call with the model included has more than about 8,000 tokens of conversation (conversation_too_long). |
| 429 | A limit reached, with Retry-After saying how long to wait: requests a minute (rate_limited), answers at once (too_many_streams), the free tier's day (daily_limit_reached), renders this month (render_limit_reached), or OpenRouter's own (openrouter_rate_limited). |
| 502 | The model failed or couldn't be reached, after the retries. |
| 503 | Accounts can't be read just now; try again. |
| 504 | The model took too long to start. |
Errors come back as `{"error": {"code", "message", "type", "metadata": {"error_type", "reason"}}}`: `type` is the reason to act on (`rate_limited`, `plan_quota_reached`...). [Limits and errors](/docs/production/limits) says what to do about each.
---
# POST /v1/render
> An answer you already have, drawn again with no model.
## Keys
Your MorphScreen key in `X-MorphScreen-Key`: the call counts against your plan.
## Example
Draw an answer you already have as HTML, with no model called:
```bash
curl https://api.morphscreen.com/v1/render \
-H "X-MorphScreen-Key: $MORPHSCREEN_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "## Budget\n\n\n"}'
```
The HTML needs no JavaScript and no style attributes, so it passes a strict content security policy. Load `morphscreen.css` from the web SDK on the page that shows it ([HTML without JavaScript](/docs/integrations/html)).
## Request
### RenderRequest
An answer you already have, in our format, with the options. output: html (the default), ops or tree. Free, counted, within a fair-use limit.
| Field | Type | Required | What it is |
|---|---|---|---|
| `text` | string | yes | |
| `packs` | list of string | | |
| `components` | list of object | | |
| `image_hosts` | list of string | | |
| `images` | list of object | | At most 200. |
| `photos` | one of the shapes below | | The app's own photo key, for one provider: with it the model may ask for photos, searched there on the app's quota, each loading from the provider's own address with its credit. Without one (or false) the model isn't asked for photos. |
| `output` | `html`, `ops`, `tree` | | |
### AppImage
One of your own pictures. The model shows it as  and the API puts its URL and credit in; its host is allowed with it.
| Field | Type | Required | What it is |
|---|---|---|---|
| `id` | string | yes | Letters, digits, '-', '_' or '.'. |
| `url` | string | yes | Where it loads from, http(s). |
| `alt` | string | | What it shows, told to the model. |
| `credit` | string | | Shown under it, as written (Markdown links allowed). |
## Responses
| Status | What comes back |
|---|---|
| 200 | The answer as HTML, ops or the tree, with the core's verdict. |
| 400 | The request can't be run as written; model_needs_your_key: a model other than the included one, asked for without your own OpenRouter key. |
| 401 | No key, or one turned down: your MorphScreen key (morphscreen_key_required, morphscreen_key_invalid) or your OpenRouter key (openrouter_key_required, openrouter_key_invalid). |
| 403 | The account is suspended (account_suspended), or the model's moderation refused the request (model_refused). |
| 413 | The body is over 1 MB (payload_too_large), or a call with the model included has more than about 8,000 tokens of conversation (conversation_too_long). |
| 429 | A limit reached, with Retry-After saying how long to wait: requests a minute (rate_limited), answers at once (too_many_streams), the free tier's day (daily_limit_reached), renders this month (render_limit_reached), or OpenRouter's own (openrouter_rate_limited). |
| 503 | Accounts can't be read just now; try again. |
Errors come back as `{"error": {"code", "message", "type", "metadata": {"error_type", "reason"}}}`: `type` is the reason to act on (`rate_limited`, `plan_quota_reached`...). [Limits and errors](/docs/production/limits) says what to do about each.
---
# GET /v1/health
> Whether the API is up.
## Keys
None.
## Example
```bash
curl https://api.morphscreen.com/v1/health
```
## Responses
| Status | What comes back |
|---|---|
| 200 | ok |
Errors come back as `{"error": {"code", "message", "type", "metadata": {"error_type", "reason"}}}`: `type` is the reason to act on (`rate_limited`, `plan_quota_reached`...). [Limits and errors](/docs/production/limits) says what to do about each.
---
# "@morphscreen/web"
> The web SDK's functions and types: loadCore, createAnswer and its options, the Answer it returns, the Core and its sessions, and the actions.
```ts
import { createAnswer, connectAgent, loadCore } from "@morphscreen/web";
import "@morphscreen/web/morphscreen.css";
```
## loadCore
```ts
loadCore(src: string | URL | ArrayBuffer | Uint8Array | Response): Promise
```
Loads the core from `morphscreen.wasm`, or `morphscreen-agent.wasm` for `connectAgent`, and starts the answer faces loading alongside. Load it once and share it.
## createAnswer
```ts
createAnswer(el: HTMLElement, options: AnswerOptions): Answer
```
Draws an answer into `el`, which gets the class `ms-answer`.
| Option | Type | What it does |
|---|---|---|
| `core` | `Core` | from `loadCore` |
| `packs` | `string[]` | packs beyond core; every one when left out, `[]` for core alone |
| `custom` | `Pack[]` | your own packs |
| `components` | `Record` | how to draw your own components: `(el, { attrs, html, text }) => cleanup?` |
| `onAction` | `(action: Action) => void` | the reader asked, submitted, finished a quiz, voted or changed state |
| `onComponentState` | `(node, state) => void` | an interactive component's state changed because the reader acted |
| `imageHosts` | `string[]` | hosts pictures may load from; any other shows its description |
| `maps` | `MapTiles` | your map provider and key |
| `maplibre` | module, or a function that loads it | MapLibre GL 6.16 or later, for live maps |
| `onOps` | `(ops: Op[]) => void` | every op, for logging or a renderer of your own |
## Answer
| Method | What it does |
|---|---|
| `push(text)` | the next piece of the model's text |
| `end(truncated?)` | the answer is over; `true` if it hit the token limit |
| `html()` | the finished answer as HTML, what a server would send |
| `applyState(states)` | sets interactive components' state, by node id |
| `destroy()` | stops and clears it |
## Action
```ts
type Action =
| { type: "ask"; text: string; node: string }
| { type: "submit"; node: string; form: string; button: string; fields: Record }
| { type: "quiz"; node: string; score: number; total: number; answers: { question: string; choice: string; right: boolean | null }[] }
| { type: "vote"; node: string; name: string; choice: string }
| { type: "state"; node: string; name: string; value: unknown };
type FieldValue = string | number | boolean | string[] | File[] | { lat: number; lng: number; accuracy: number };
```
[Actions and state](/docs/format/actions) says when each is sent.
## Core
| Method | What it does |
|---|---|
| `prompt(packs?, custom?)` | the system prompt for that catalog |
| `session(packs?, custom?)` | a `Session` that reads one answer |
| `actionMessage(action)` | the user message an action starts, worded as the API words it |
## Session
For reading an answer yourself, without drawing it:
| Method | What it does |
|---|---|
| `push(text)` | feeds the next piece; returns the ops it produced |
| `finish(truncated?)` | the answer is over; returns the last ops |
| `html()`, `tree()` | the answer as HTML, or as its tree |
| `verdict()` | the core's verdict: valid, truncated, the component count and the diagnostics |
| `allowImages(hosts)` | where the HTML's pictures may load from |
## Other exports
- `connectAgent(el, options)`: a conversation with an AG-UI agent ([its reference](/docs/reference/agent)).
- `allowedImage(src, { imageHosts })`: whether a picture's address may load.
- `enhanceCharts(root)`: makes the charts under `root` respond (tooltips, legend items that switch a series off), for HTML from the server; returns a function that undoes it.
The package also has `@morphscreen/web/react` ([React](/docs/reference/react)) and `@morphscreen/web/theme` ([theme](/docs/reference/theme)).
---
# "@morphscreen/web/react"
> The Answer component for React 18 and later, and the props for your own components.
```tsx
import { Answer, type CustomProps } from "@morphscreen/web/react";
;
```
## Answer
| Prop | Type | What it is |
|---|---|---|
| `core` | `Core` | from `loadCore` |
| `text` | `string` | the answer so far; when it grows, only the new part is read |
| `stream` | `AsyncIterable` | or the chunks as they arrive |
| `done` | `boolean` | the answer is complete |
| `packs` | `string[]` | packs beyond core |
| `custom` | `Pack[]` | your own packs |
| `components` | `Record>` | React components for your own components, by name |
| `onAction` | `(action: Action) => void` | what the reader did |
| `imageHosts` | `string[]` | hosts pictures may load from |
| `maps` | `MapTiles` | your map provider and key |
| `maplibre` | module | MapLibre GL, for live maps |
| `className` | `string` | your classes for the answer's element, which keeps `ms-answer` too |
## CustomProps
```ts
type CustomProps = { attrs: Attrs; html: string; text: string };
```
`attrs` are the component's attributes as the model wrote them, and `html` and `text` its content. [React](/docs/integrations/react) has a whole example.
---
# connectAgent
> A whole conversation with an AG-UI agent, drawn as it streams, with the reader's actions sent back and the state shared.
```ts
connectAgent(el: HTMLElement, options: AgentOptions): Conversation
```
It needs the core from `morphscreen-agent.wasm`. Every option of [`createAnswer`](/docs/reference/web#createanswer) applies to each answer, except `onOps`.
## AgentOptions
| Option | Type | What it does |
|---|---|---|
| `url` | `string` | the agent's endpoint: a run's input is POSTed, its events read back |
| `headers` | object, or a function returning one | request headers, like your own auth, for each run |
| `threadId` | `string` | the conversation's id; a new one when left out |
| `tools`, `context`, `forwardedProps` | | sent with every run, as AG-UI defines them |
| `onToolCall` | `(call) => result` | answers a tool of yours the agent called; the conversation then goes on |
| `onUpdate` | `(update) => void` | every update from the core: steps, custom events, warnings |
| `onState` | `(state) => void` | the shared state changed, by the agent or the reader |
| `sendActions` | `boolean` | send the reader's actions on as user messages (default `true`) |
## Conversation
| Member | What it does |
|---|---|
| `send(content, id?)` | the reader says something (text, or AG-UI content parts); runs the agent |
| `run()` | runs the agent on the conversation as it is |
| `stop()` | stops the run in progress, which ends as truncated |
| `messages()`, `state()` | what to save |
| `load(messages, state?)` | starts from a saved conversation |
| `setState(state)` | sets the shared state |
| `threadId`, `running` | the conversation's id, and whether a run is going |
| `destroy()` | stops and clears it |
[AG-UI agents](/docs/agents/ag-ui) has the guide.
---
# "@morphscreen/web/theme"
> Apply a W3C Design Tokens file to every component, light, dark and mono, and check its contrast before you ship it.
```ts
import { applyTheme } from "@morphscreen/web/theme";
import tokens from "./brand.tokens.json";
applyTheme(tokens); // every answer on the page
applyTheme(tokens, { scope: "#support" }); // only answers inside #support
```
A theme is a [W3C Design Tokens](https://www.designtokens.org) file: the colours of each role in light, dark and mono, the faces, the type steps and the corners. A file that leaves a role out is an error, so a theme is always complete. The page's `data-theme` still picks the mode. Applying again with the same scope replaces the earlier theme.
## Functions
| Function | What it does |
|---|---|
| `applyTheme(tokens, { scope?, document? })` | puts the theme on the page after the SDK's stylesheet, as a `