Declarative UI with A2UI
By the Syncfusion DevTutorials team · Last updated
The agent sends a JSON description of the UI. Build a renderer that only draws catalogue components with valid props, renders parts as they stream, and sends user events back with IDs.
What Declarative UI Is
Declarative UI = the agent sends a description of the interface as data (which parts, in what order, with what values), and your app draws it with its own components.
In this lesson, you will build a renderer for A2UI payloads that draws a refund form as it streams, refuses anything unsafe, and tells the agent exactly which button the user pressed.
In Lesson 7, the agent could only pick one whole component. With declarative UI, it can lay out a form: a title, a field, a button. That is more flexible.
It is also riskier. The payload comes from a model, and a model can be tricked. Here is a payload the naive renderer accepts:
Press Play, or use Next to go one step at a time.
- Frame 1: an HtmlBlock that posts card numbers to another site. Only the allow-list stops it.
- Frame 2: a normal Button with an extra
onClickprop holding code. Only the schema stops it. - Frame 3: a broken TextField. With no checks, it renders broken.
Simple lesson: a renderer must decide what is safe to draw. The agent does not get to.
A Real-World Example
Think about flat-pack furniture.
- The instructions say which parts go where: "attach shelf B to side A with 4 screws".
- You only use the parts in the box.
- If the instructions mention a part that is not in the box, you stop. You don't make one up from a kitchen knife.
A2UI works the same way:
- The instructions = the JSON payload from the agent.
- The parts in the box = your component catalogue.
- Stopping at a missing part = rejecting what is not in the catalogue.
Click the button. The Build tab plays the hostile button through the naive renderer.
- The screen: "Claim your gift" looks like any other button.
- Side effects: when the user presses it, the code in
onClickruns. - The checks: no code from the payload may run.
Simple lesson: the payload is a request. Your renderer decides.
What You Will Learn
In this lesson, you will learn:
- What A2UI is, and why it is "data, not code".
- What a component catalogue is.
- Why you need both an allow-list and schema validation.
- How to render a payload as it streams.
- How to send user events back to the agent.
About A2UI in this lab
- A2UI is an open protocol from Google for agents to describe UI as JSON. Version 0.9 shipped in April 2026, and a v1.0 candidate exists.
- The lab uses a small, pinned subset: Card, Text, TextField and Button. Real A2UI has more parts and a richer message format.
- The rules you practise here (allow-list, schema, events with IDs) apply to the full spec too.
Data, Not Code
A2UI payload = JSON that lists components, each with an id, a type and props.
{
"surfaceId": "refund-1",
"components": [
{ "id": "t1", "type": "Text", "props": { "text": "Refund for order #A-1042" } },
{ "id": "f1", "type": "TextField", "props": { "label": "Amount", "value": "£29.99" } },
{ "id": "b1", "type": "Button", "props": { "label": "Request refund", "action": "submit-refund" } }
]
}
There is no HTML and no script. The client draws each part with its own native component: a React component on the web, a Flutter widget on mobile.
That is the security idea: the agent can only send data. As long as your renderer only accepts data, the agent cannot run code.
Simple lesson: if nothing in the payload can run, nothing in the payload can attack.
Two Checks, Two Attacks
The Allow-List
Component catalogue = the list of component types your renderer will draw. The allow-list check rejects any type not on it.
It stops the HtmlBlock: the type was never audited, so it never renders.
Schema Validation
Schema = the props each type accepts, and their types.
It stops the hostile Button: Button is allowed, but onClick is not one of its props. It also stops the broken TextField: no id, and a number where text belongs.
function acceptComponent(c: A2Comp): string | null {
const schema = CATALOG[c.type];
if (!schema) return `${c.type} is not in the catalogue`;
if (!c.id) return "missing id";
for (const k of Object.keys(schema)) if (typeof c.props[k] !== "string") return `${k} must be text`;
const extra = Object.keys(c.props).filter((k) => !(k in schema));
return extra.length ? `unknown prop ${extra[0]}` : null;
}
One check alone is not enough.
- The allow-list misses a bad prop on an allowed type.
- The schema misses a well-formed type you never audited.
- Use both, on every component, every time.
Simple lesson: the allow-list checks the type; the schema checks the props.
Render as It Streams
A2UI is built to stream. The agent sends a surface in parts, and the client updates as each part arrives.
Step through all three frames.
- Frames 1 and 2: on the left, the renderer waits for the whole payload. On the right, each valid part appears at once.
- Frame 3: both now show the full form.
Rendering parts as they arrive is the same idea as streaming text in Lesson 2: show progress from the first second.
Simple lesson: validate each part as it arrives, and draw it straight away.
Events Back to the Agent
When the user presses a button, the agent needs to know which one, on which surface.
Step through both frames.
- Frame 1: the event is just
{ type: "click" }. The agent has to guess. - Frame 2: the event carries
surfaceId,componentIdandaction. The agent knows exactly what happened.
Easy way to remember: In the catalogue? Valid props? Then draw it. Pressed? Say which.
Simple lesson: every user event names its surface and component.
Lab Exercise: Build a Safe A2UI Renderer
Your task: change the Lesson 8 settings in the Build tab so all four scenarios pass. Then press Check all scenarios.
Click the button and open the Build tab.
- The four scenarios: a valid streamed form, a broken payload, an unlisted component and a hostile button.
- Rejected box: shows each part your renderer refused, and why.
- Event sent to the agent: appears when the user presses a button.
Simple lesson: allow-list, schema, streaming and event IDs, all four.
What Changes per Framework
| Option | Declarative UI |
|---|---|
| Hand-built React | acceptComponent and a small renderer, as above |
| A2UI renderers | A2UI renders natively on web, Flutter, Android and iOS |
| CopilotKit | Renders A2UI payloads carried over AG-UI |
| Open-JSON-UI | Another JSON UI format, for comparison only |
Build It in React
In the Code tab, A2UI parts arrive as a2ui events. Each part is data from the agent, so check it before you draw it:
const CATALOGUE: Record<string, string[]> = { Card: [], Text: ["text"], TextField: ["label", "value"], Button: ["label", "action"] };
function valid(c: A2Part) {
const allowed = CATALOGUE[c.type];
return !!allowed && !!c.id && Object.keys(c.props).every((k) => allowed.includes(k));
}
Render valid parts the moment they arrive. When a button is pressed, call agent.sendEvent() with the surface ID, the component ID and the action. Never run anything found inside a prop.
TL;DR
- Declarative UI = the agent describes UI as data; your app draws it.
- A2UI = an open JSON protocol for that. Data, not code.
- Catalogue + allow-list = only audited types render.
- Schema = only known props, with the right types.
- Stream = validate and draw each part as it arrives.
- Events = send surface ID, component ID and action back.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Passing props through | "Spread all props onto the component." | Only pass props the schema names. | An onClick string becomes code. |
| Allow-list only | "Button is safe, so any Button is safe." | Validate props too. | A hostile prop rides in on an allowed type. |
| An HtmlBlock "just for now" | "We'll lock it down later." | Never put raw HTML in the catalogue. | The agent can draw any form, including a phishing one. |
| Waiting for the whole payload | "Render when it's complete." | Render valid parts as they arrive. | Long blank waits, like Lesson 2's spinner. |
| Events without IDs | "The agent knows what it sent." | Send surface and component IDs. | The agent acts on the wrong button. |
Check Your Understanding
Exercises
What to Remember
- Declarative UI: the agent describes, your app draws.
- A2UI is data, not code. Keep it that way.
- The allow-list checks the type. The schema checks the props. Use both.
- Render each valid part as it streams.
- Send surface and component IDs with every event.
Further Reading
- A2UI v0.9: the new standard for portable, framework-agnostic generative UI, by Google Developers Blog: the spec version this lab follows.
- What is Generative UI?, by Google Cloud: why data-not-code is the security model.
Next: Open-ended UI and MCP Apps, where a partner sends full HTML and you keep it in a sandbox it cannot escape.