Reference client-side A2UI starter for the browser. Uses Google's official @a2ui/web_core state machine + @a2ui/lit renderer, extended with a custom catalog. No bundler, no backend state. Claude via a tiny proxy.
No install. Tap an option or type something β Claude streams replies into A2UI surfaces in real time. Top-right Show A2UI wire log to watch every v0.9 message go by. Top-right Kitchen sink to see every component in the catalog render at once.
Companion to a2ui-starter-swiftui β same four skills, same skeleton-first streaming pattern, same progressive-rendering RFC primitives.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β index.html (self-contained, no build step) β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β A2UIStarter (Lit element β chat shell) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β SkillRuntime β routes user events to skills, β β
β β streams Claude's reply, emits v0.9 messages β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β processMessages([...]) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β @a2ui/web_core MessageProcessor β β
β β β validates every message against starterCatalog β β
β β β owns all surface state (SurfaceModel per turn) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β renders via β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β @a2ui/lit <a2ui-surface> β β
β β + basicCatalog (18 components) β β
β β + OptionsGrid (extension β Lit + zod) β β
β β + RichMessageCard (extension β Lit + zod) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β fetch() POST /v1/messages (SSE)
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β proxy/llm-proxy.js (Cloudflare Worker, ~70 lines) β
β β holds ANTHROPIC_API_KEY as a Worker secret β
β β forwards to api.anthropic.com, streams SSE back β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
https://api.anthropic.com/v1/messages
Three packages load from esm.sh via an importmap β no bundler, no npm install:
@a2ui/web_core@0.9.2, @a2ui/lit@0.9.3, zod@3.25.76, plus lit and js-yaml.
Every user-visible turn flows through Google's MessageProcessor. Each agent reply is a sequence of v0.9 wire messages:
{"version":"v0.9", "createSurface":{"surfaceId":"msg_1","catalogId":"a2ui-starter/core@0.1"}}
{"version":"v0.9", "updateDataModel":{"surfaceId":"msg_1","value":{"reply":{}}}}
{"version":"v0.9", "updateComponents":{"surfaceId":"msg_1","components":[...]}}
{"version":"v0.9", "updateDataModel":{"surfaceId":"msg_1","path":"/reply/intro","value":"..."}}Click "Show A2UI wire log" in the running app (top-right button) to watch these fly by in real time.
git clone https://github.com/vpm238/a2ui-starter-web
cd a2ui-starter-web
# Terminal 1 β Anthropic proxy on :8787 (keeps your API key off the browser)
export ANTHROPIC_API_KEY="sk-ant-..."
python3 proxy/local-proxy.py
# Terminal 2 β static server on :5173
python3 -m http.server 5173
# Open http://localhost:5173The app auto-detects localhost and routes LLM calls through http://localhost:8787. Override with ?proxy=https://... if you want.
python3 -m http.server 5173
# Open http://localhost:5173
# First load: prompt for API key β stored in localStorage
# Clear with `localStorage.clear()` in DevTools when done-
Deploy the proxy (see proxy/README.md):
cd proxy npm install -g wrangler wrangler secret put ANTHROPIC_API_KEY wrangler deploy # Copy the printed URL, e.g. https://a2ui-llm-proxy.you.workers.dev
-
Point the web app at the proxy. At the top of
index.html, inside<head>, add:<script>window.A2UI_PROXY_URL = 'https://a2ui-llm-proxy.you.workers.dev';</script>
-
Enable GitHub Pages at
Settings β Pages:- Source: Deploy from a branch
- Branch:
main, folder:/(root) - Save
~60 seconds later, your starter is live at https://<you>.github.io/a2ui-starter-web/.
starterCatalog = new Catalog('a2ui-starter/core@0.1', [
...basicCatalog.components, // 18 official components
A2uiOptionsGrid, // extension β tap-to-fire options
A2uiRichMessageCard, // extension β opinionated recommendation
])basicCatalog ships: Text, Button, TextField, Row, Column, List, Image, Icon, Video, AudioPlayer, Card, Divider, CheckBox, Slider, DateTimeInput, ChoicePicker, Tabs, Modal.
Our two extensions add UX patterns the basic set doesn't cover: a stacked list where each row fires an event (common for agent intake) and a strong-take recommendation card. Both are full-fidelity A2UI components β schema-validated, data-bound, action-dispatching β defined inline in index.html (sections 1 & 2, ~110 lines each).
See the Kitchen sink button in the running app for a live render of all 20 components.
Four LLM-backed skills + one static kitchen sink, all in skills/ as SKILL.md files (YAML frontmatter + markdown body):
| Skill | Trigger | What it does |
|---|---|---|
greeting |
(default) | Static intake β routes to one of the three below |
planner |
want_plan |
Breaks a goal into 3 concrete first steps |
decider |
want_decision |
Weighs two options. Takes a position |
critic |
want_feedback |
One strong opinionated piece of feedback |
kitchen |
show_kitchen_sink |
Renders every component in the catalog |
The frontmatter's first_turn_skeleton.components is the initial A2UI component tree. first_turn_fill_fields names the data-model paths Claude streams into. The markdown body below the fence is the skill's system prompt.
See skills/README.md for the format spec.
Anthropic SSE β FieldParser β per-field deltas β updateDataModel with set (accumulated value). Client-side typewriter smoothing splits chunks into char-paced updates so the rendering feels like typing instead of popping. Override the pace via ?smooth=N (chars/sec; default 220; ?smooth=0 disables).
RFC Proposal 3's append patch op would be more efficient on the wire β the official MessageProcessor only supports set in v0.9, so we send growing accumulated values. When append lands, swap _setSmoothly for an append-op emitter and the wire traffic drops by ~90%.
a2ui-starter-web/ # repo root is also GH Pages root
βββ README.md # you are here
βββ LICENSE # MIT
βββ index.html # the whole app (~930 lines)
βββ official-test.html # standalone 5-step renderer sanity check
βββ skill.manifest.json # experimental host manifest
βββ skills/ # SKILL.md per skill, loaded at runtime
β βββ README.md # SKILL.md format
β βββ greeting.md # intake β no LLM call
β βββ planner.md # β 3 ordered steps
β βββ decider.md # β two-option comparison
β βββ critic.md # β strong-take card
β βββ kitchen.md # static kitchen sink
βββ catalog/
β βββ catalog.json # reference schema for the extension components
βββ proxy/ # Anthropic proxy
βββ README.md
βββ llm-proxy.js # Cloudflare Worker (deploy with wrangler)
βββ local-proxy.py # zero-dep local dev
βββ wrangler.toml
| SwiftUI starter | Web starter (this) | |
|---|---|---|
| Platform | macOS / iOS / visionOS | Any modern browser |
| Build step | swift run |
None β open index.html |
| Renderer | a2ui-swiftui (own) |
@a2ui/lit (Google official) + 2 extensions |
| Skill runtime | a2ui-skills-swiftui (own) |
Inline SkillRuntime + FieldParser |
| LLM provider | Direct via AnthropicLLMProvider |
Cloudflare Worker proxy (key stays server-side) |
| Catalog | 13 components (all inline) | 18 from basicCatalog + 2 extensions |
Same protocol. Same skill shapes. Same streaming UX.
- A modern browser (Chrome/Safari/Firefox 2024+)
- An Anthropic API key
- For shipping: a Cloudflare account (free tier covers this)
MIT. See LICENSE.
a2ui-swiftuiβ Swift/SwiftUI renderer librarya2ui-skills-swiftuiβ Swift skill runtimea2ui-starter-swiftuiβ Swift reference appa2ui-progressive-rendering-rfcβ RFC + demo for streaming primitives- Google A2UI spec β the protocol