A2UI is one of the most interesting protocol projects in the current agent stack because it attacks a problem most teams hit almost immediately: plain chat is a terrible UI for forms, approvals, review flows, editable artifacts, and step-by-step workflows.
I already covered A2UI briefly in “From Chat to Agent UI”. This article is the deeper, implementation-first follow-up: what A2UI actually is, where it is already being used, and how to build with it from a minimal React prototype all the way to transport-level integration.
TL;DR
- A2UI is a declarative UI protocol, not a frontend framework. Your agent sends JSON that describes UI. Your client renders that JSON using trusted native components.
- The core value is simple: safe like data, expressive like code. The agent does not ship HTML or JavaScript into your app.
- As of April 6, 2026, the A2UI docs list React as stable for v0.8, while Lit, Angular, and Flutter have broader version coverage. If you want a practical React starting point today, use the stable v0.8 renderer shape.
- A2UI is already showing up in official Google and partner surfaces: Opal, Gemini Enterprise, Flutter GenUI SDK, Google ADK, and official open-source examples such as Restaurant Finder, Contact Lookup, and the Personalized Learning demo.
- The right learning path is:
- render a static surface locally
- add data binding and user actions
- move message generation to a backend
- connect A2UI to a real transport such as A2A, AG-UI, or MCP
- Choose A2UI when the agent and UI are separated by a trust boundary, when you need the same agent response to render across platforms, or when your workflow depends on forms, cards, lists, pickers, and approval surfaces instead of endless back-and-forth text.
What You Will Learn Here
- What A2UI is, and what it is not
- How A2UI fits with A2A, AG-UI, and MCP
- Where A2UI is already being used in real systems
- How to render your first A2UI surface in React
- How data binding and user actions work
- How to move from a local demo to a server-driven app
- How to think about advanced patterns such as MCP integration, catalog negotiation, and versioning
What A2UI Actually Is
The cleanest current definition comes from the official docs: A2UI is a declarative UI protocol for agent-driven interfaces. Instead of the agent sending executable UI code, it sends JSON messages describing surfaces, components, data bindings, and actions.
That matters because many serious agent systems are not local, single-process apps. The agent may be:
- running remotely
- owned by another service or team
- updating the UI asynchronously
- serving multiple clients across web, mobile, and desktop
If that agent sends raw HTML or JavaScript, you immediately inherit security, portability, and styling problems.
A2UI takes a different path:
- the agent chooses the UI structure
- the client owns the rendering
- the catalog defines what components are allowed
- the transport is separate from the UI protocol
That gives you a safer and more portable model.
Why This Problem Exists
The official A2UI docs use a booking flow to make the point, and it is a good one.
This is the bad version:
User: Book a table for 2 tomorrow at 7pm.
Agent: Sure. Which day?
User: Tomorrow.
Agent: What time?
User: 7pm.
Agent: Indoor or outdoor?
User: Outdoor.
This is the better version:
User: Book a table for 2 tomorrow at 7pm.
Agent:
-> renders a date/time picker
-> pre-fills party size
-> shows an outdoor seating checkbox
-> attaches a confirm button
That is the shift A2UI is trying to standardize.
Real-World A2UI Examples
One thing I like about A2UI right now is that the project is not only a spec repo. The official docs already show where it is landing.
1. Google Opal
The official “A2UI in the World” page says Opal uses A2UI to power the dynamic, generative UI system behind its AI mini-apps. The interesting part is not just “AI makes widgets.” It is that A2UI gives Opal a way to let AI drive UI structure without treating the frontend like an arbitrary-code sandbox.
2. Gemini Enterprise
The same official page says Gemini Enterprise is integrating A2UI for:
- data-entry forms
- approval dashboards
- workflow automation
- task-specific enterprise UI
That is exactly the kind of workflow where chat alone becomes awkward.
3. Flutter GenUI SDK
According to the official docs, Flutter GenUI SDK uses A2UI under the hood. That is a strong signal that A2UI is not just a web-only curiosity. It is already being used as a cross-platform server-driven UI layer.
4. Google ADK
The docs also say Google ADK’s developer UI renders A2UI natively and handles A2UI-to-A2A conversion. That is important because it shows A2UI is not an isolated experiment. It is becoming part of a broader agent-development toolchain.
5. Official Open-Source Samples
The A2UI docs and repo point to several concrete examples:
- Restaurant Finder for reservation-style dynamic forms
- Contact Lookup for search plus structured results
- Personalized Learning for flashcards, quizzes, and grounded educational content
The Personalized Learning sample is especially useful because it is a full-stack example, not just a component gallery. It combines:
- a frontend renderer
- a remote agent
- personalized learner context
- textbook retrieval
- A2UI-generated flashcards and quiz cards
That is a strong real-world reference point because it shows A2UI being used for actual structured learning artifacts, not just generic cards.
A Better Mental Model
The easiest way to think about A2UI is this:
Agent decides WHAT surface should exist
Client decides HOW that surface is rendered
Transport decides HOW messages move between them
In practice the stack often looks like this:
User
-> frontend app
-> transport layer (A2A / AG-UI / MCP / SSE / WebSocket)
-> agent backend
-> A2UI messages
-> renderer
-> native UI components
-> user action
-> backend again
That also helps separate A2UI from nearby protocols:
- A2UI: declarative UI payloads
- A2A: agent-to-agent transport and coordination
- AG-UI: high-bandwidth agent-to-frontend interaction protocol
- MCP: tool, resource, and data connectivity for models and agents
One nice line from the AG-UI docs is that A2UI is a generative UI specification, while AG-UI is the agent-user interaction protocol. Those are complementary, not competing, layers.
Versions Matter More Than You Think
This is the first practical detail I would want someone to tell me before I started.
As of April 6, 2026:
- v0.8 is the stable A2UI protocol
- v0.9 is draft
- v0.10 exists in the repo but is still under development
- the official React renderer is stable for v0.8
That means the smartest way to learn A2UI today is:
- use v0.8-style messages for runnable React examples
- understand that the newer specs are flattening and cleaning up the message model
- avoid mixing version shapes in the same app
I will do exactly that below: the runnable React examples use v0.8, and the advanced transport section will point out how newer specs differ.
Step 1: Build the Smallest Possible A2UI App
If you only want to understand the shape of the protocol, the fastest path is a plain React app with the official renderer.
Install
npm create vite@latest a2ui-hello -- --template react-ts
cd a2ui-hello
npm install
npm install @a2ui/react
src/App.tsx
This is the smallest useful example I know that still shows the whole idea.
import { A2UIViewer, injectStyles } from "@a2ui/react";
import type { Types } from "@a2ui/react";
injectStyles();
const messages: Types.ServerToClientMessage[] = [
{
surfaceUpdate: {
surfaceId: "main",
components: [
{
id: "title",
component: {
Text: {
text: { literalString: "Book Your Table" },
usageHint: "h1",
},
},
},
{
id: "body",
component: {
Text: {
text: {
literalString:
"The agent did not send HTML. It sent structured UI data.",
},
usageHint: "body",
},
},
},
{
id: "cta-label",
component: {
Text: {
text: { literalString: "Start booking" },
usageHint: "body",
},
},
},
{
id: "cta-button",
component: {
Button: {
child: "cta-label",
action: {
name: "start_booking",
context: [
{
key: "flow",
value: { literalString: "restaurant" },
},
],
},
},
},
},
{
id: "root",
component: {
Column: {
children: {
explicitList: ["title", "body", "cta-button"],
},
distribution: "start",
alignment: "stretch",
},
},
},
],
},
},
{
beginRendering: {
surfaceId: "main",
root: "root",
},
},
];
export default function App() {
return (
<main style={{ maxWidth: 720, margin: "48px auto", padding: 24 }}>
<A2UIViewer
messages={messages}
onAction={(event) => {
console.log("A2UI action:", event.userAction);
}}
/>
</main>
);
}
That one file teaches the most important A2UI ideas:
- a surface is the render target
- the UI is a flat list of components
- relationships are expressed by IDs
- rendering starts when the client gets
beginRendering - a
Buttonemits a user action
You have not built a “chatbot widget.” You have built a small agent-driven UI surface.
Step 2: Add Real Form State and User Actions
The next thing you need is data binding. This is where A2UI starts feeling different from a static JSON renderer.
In A2UI, inputs can bind to a data model. When the user changes a field, the client updates local state. When the user clicks a button, the action can include values resolved from that data model.
That means the agent can render a form without hardcoding every interaction into the frontend.
src/reservationMessages.ts
import type { Types } from "@a2ui/react";
export const reservationMessages: Types.ServerToClientMessage[] = [
{
dataModelUpdate: {
surfaceId: "booking",
path: "/",
contents: [
{
key: "booking",
valueMap: [
{ key: "name", valueString: "" },
{ key: "partySize", valueString: "2" },
{ key: "when", valueString: "2026-04-12T19:00:00Z" },
{ key: "outdoor", valueBoolean: false },
],
},
],
},
},
{
surfaceUpdate: {
surfaceId: "booking",
components: [
{
id: "title",
component: {
Text: {
text: { literalString: "Restaurant Reservation" },
usageHint: "h2",
},
},
},
{
id: "nameField",
component: {
TextField: {
label: { literalString: "Name" },
text: { path: "booking.name" },
},
},
},
{
id: "partyField",
component: {
TextField: {
label: { literalString: "Party size" },
text: { path: "booking.partySize" },
},
},
},
{
id: "whenField",
component: {
DateTimeInput: {
value: { path: "booking.when" },
enableDate: true,
enableTime: true,
},
},
},
{
id: "outdoorField",
component: {
CheckBox: {
label: { literalString: "Outdoor seating" },
value: { path: "booking.outdoor" },
},
},
},
{
id: "submitLabel",
component: {
Text: {
text: { literalString: "Confirm reservation" },
usageHint: "body",
},
},
},
{
id: "submitButton",
component: {
Button: {
child: "submitLabel",
action: {
name: "confirm_reservation",
context: [
{ key: "name", value: { path: "booking.name" } },
{ key: "partySize", value: { path: "booking.partySize" } },
{ key: "when", value: { path: "booking.when" } },
{ key: "outdoor", value: { path: "booking.outdoor" } },
],
},
},
},
},
{
id: "root",
component: {
Column: {
children: {
explicitList: [
"title",
"nameField",
"partyField",
"whenField",
"outdoorField",
"submitButton",
],
},
distribution: "start",
alignment: "stretch",
},
},
},
],
},
},
{
beginRendering: {
surfaceId: "booking",
root: "root",
},
},
];
src/App.tsx
import { useEffect } from "react";
import {
A2UIProvider,
A2UIRenderer,
injectStyles,
useA2UIActions,
} from "@a2ui/react";
import type { Types } from "@a2ui/react";
import { reservationMessages } from "./reservationMessages";
injectStyles();
function BookingSurface() {
const { processMessages, clearSurfaces } = useA2UIActions();
useEffect(() => {
clearSurfaces();
processMessages(reservationMessages);
}, [clearSurfaces, processMessages]);
return <A2UIRenderer surfaceId="booking" />;
}
export default function App() {
return (
<A2UIProvider
onAction={(event: Types.A2UIClientEventMessage) => {
console.log("Send this to your backend:", event.userAction);
alert(JSON.stringify(event.userAction, null, 2));
}}
>
<main style={{ maxWidth: 720, margin: "48px auto", padding: 24 }}>
<BookingSurface />
</main>
</A2UIProvider>
);
}
This is the first point where A2UI becomes genuinely useful.
Notice what the frontend is not doing:
- it is not manually wiring each input into local React state
- it is not manually assembling a
confirm_reservationpayload - it is not hardcoding which surface to show next
The frontend is mostly acting as:
- renderer
- local data-binding runtime
- action dispatcher
That is a very different division of responsibility from a normal form app.
Step 3: Make the UI Server-Driven
Once you understand the local flow, the next step is to move message generation to the backend.
That is where A2UI becomes interesting in real systems, because now the agent can decide:
- whether the next step should be plain text or structured UI
- which surface to show
- which fields to prefill
- which actions should come back to the server
Here is a deliberately small Node example.
server.ts
import express from "express";
import { reservationMessages } from "./src/reservationMessages";
const app = express();
app.use(express.json());
app.post("/api/agent", (req, res) => {
const input = req.body.message;
// Initial prompt from the user.
if (typeof input === "string") {
return res.json(reservationMessages);
}
// Action emitted by A2UI after the user clicks Confirm reservation.
if (input?.userAction?.name === "confirm_reservation") {
const context = input.userAction.context ?? {};
const outdoorText = context.outdoor ? "outdoor" : "indoor";
return res.json([
{
surfaceUpdate: {
surfaceId: "booking",
components: [
{
id: "doneTitle",
component: {
Text: {
text: {
literalString: `Reservation created for ${context.name || "guest"}`,
},
usageHint: "h2",
},
},
},
{
id: "doneBody",
component: {
Text: {
text: {
literalString: `${context.partySize || "2"} guests on ${context.when}. Seating: ${outdoorText}.`,
},
usageHint: "body",
},
},
},
{
id: "root",
component: {
Column: {
children: {
explicitList: ["doneTitle", "doneBody"],
},
distribution: "start",
alignment: "stretch",
},
},
},
],
},
},
{
beginRendering: {
surfaceId: "booking",
root: "root",
},
},
]);
}
return res.status(400).json({ error: "Unknown message type" });
});
app.listen(3001, () => {
console.log("Agent server listening on http://localhost:3001");
});
src/App.tsx
import { useEffect, useState } from "react";
import { A2UIViewer, injectStyles } from "@a2ui/react";
import type { Types } from "@a2ui/react";
injectStyles();
export default function App() {
const [messages, setMessages] = useState<Types.ServerToClientMessage[]>([]);
async function roundTrip(message: string | Types.A2UIClientEventMessage) {
const response = await fetch("/api/agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
const nextMessages: Types.ServerToClientMessage[] = await response.json();
setMessages(nextMessages);
}
useEffect(() => {
void roundTrip("Book a table for two tomorrow at 7pm");
}, []);
return (
<main style={{ maxWidth: 720, margin: "48px auto", padding: 24 }}>
<A2UIViewer messages={messages} onAction={roundTrip} />
</main>
);
}
This is the first architecture that feels production-shaped:
user prompt
-> backend decides UI
-> backend sends A2UI messages
-> client renders them
-> user acts on native UI
-> client sends action back
-> backend returns the next surface
That is a clean foundation for:
- approvals
- guided setup
- search + filter results
- booking flows
- structured troubleshooting
- artifact review
Real Example Pattern: Personalized Learning
The official A2UI Personalized Learning demo is worth studying because it shows where this approach gets much stronger than “UI from JSON.”
The sample combines:
- a chat shell
- a remote agent
- OpenStax content retrieval
- learner-specific context
- custom A2UI components like Flashcard and QuizCard
The high-level flow looks like this:
Student prompt
-> API server
-> remote agent
-> content retrieval + learner profile
-> A2UI flashcards / quiz cards
-> frontend renderer
That pattern is more important than the education domain itself.
You can reuse the same architecture for:
- onboarding checklists
- contract review cards
- procurement approval dashboards
- incident response runbooks
- internal training flows
That is my inference from the official samples and deployment notes, but it is a pretty safe one: A2UI is strongest when the “answer” should actually be a working surface.
Advanced Pattern 1: Multiple Surfaces and Progressive Rendering
One of the strongest protocol ideas in A2UI is that structure and data are separate. That means you can:
- create a surface early
- render a skeleton immediately
- update data later
- swap or delete surfaces when the workflow changes
This is ideal for agent workflows that take time.
For example:
1. Agent creates a "review" surface with heading + loading state
2. Agent streams tool work in the background
3. Agent updates the data model with fetched results
4. Agent swaps the CTA from "Loading..." to "Approve deployment"
This is much nicer than forcing the user to wait for one giant final response.
Advanced Pattern 2: A2UI Over MCP
The official A2UI docs now include a guide for A2UI over MCP, and this is one of the clearest examples of why the protocol is useful beyond a single app.
The idea is:
- an MCP tool returns regular text plus an embedded resource
- the embedded resource uses
application/json+a2ui - the client routes that resource to its A2UI renderer
That lets a tool response open into actual UI instead of a blob of JSON.
Minimal MCP Example in Python
import json
import mcp.types as types
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("a2ui-demo")
@mcp.tool()
def get_training_plan_ui():
a2ui_payload = [
{
"version": "v0.10",
"createSurface": {
"surfaceId": "default",
"catalogId": "https://a2ui.org/specification/v0_10/basic_catalog.json",
},
},
{
"version": "v0.10",
"updateComponents": {
"surfaceId": "default",
"components": [
{
"id": "root",
"component": "Text",
"text": "Hello from A2UI over MCP!",
}
],
},
},
]
a2ui_resource = types.EmbeddedResource(
type="resource",
resource=types.TextResourceContents(
uri="a2ui://training-plan",
mimeType="application/json+a2ui",
text=json.dumps(a2ui_payload),
),
)
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text="Here is your generated training plan summary.",
),
a2ui_resource,
]
)
That is a very powerful pattern because the tool can now return:
- normal assistant narration
- a structured UI artifact
- a user action path back into the server
The A2UI over MCP guide also calls out catalog negotiation, which is easy to miss but extremely important. Before a server sends A2UI, client and server need to agree on:
- the supported protocol version
- the supported catalog IDs
- whether custom or inline catalogs are allowed
That agreement is what keeps the UI safe and predictable.
Advanced Pattern 3: Understand the Version Shift
If you read the latest specs, you will notice newer versions flatten the message model.
In stable v0.8, you will often see:
surfaceUpdatedataModelUpdatebeginRendering
In newer versions, that evolves toward:
createSurfaceupdateComponentsupdateDataModeldeleteSurfaceactionResponse
This is not just cosmetic. It makes the lifecycle more explicit and transport integrations cleaner.
My practical advice is:
- build today on the stable renderer version you can actually run
- keep your server message generation isolated behind a small adapter
- do not leak raw protocol shape all over your app
That way, moving from one A2UI version to another becomes a backend concern instead of a product rewrite.
When A2UI Is the Right Choice
Choose A2UI when:
- the agent is remote or untrusted
- the same UI needs to render across web, mobile, and desktop
- you want the agent to generate forms, cards, lists, checklists, tabs, pickers, or review surfaces
- you care about native rendering rather than iframe-style isolation
- your team wants the client to retain control over styling, accessibility, and security
Do not choose A2UI when:
- the UI is mostly static product UI that you already own
- there is no real need for server-driven or agent-generated interaction surfaces
- you only need a chat bubble with occasional markdown
- you are trying to replace your whole frontend architecture with a protocol for one narrow workflow
Common Mistakes
1. Treating A2UI like a design system
A2UI is not your design system. It is the protocol between the agent and the renderer. Your design system still belongs to the client.
2. Mixing protocol versions casually
This is the easiest way to confuse yourself. Pick one version and stay consistent within a given app.
3. Shipping giant surfaces before proving the loop
Start with:
- one surface
- one or two components
- one action
- one backend round-trip
That is enough to validate the architecture.
4. Forgetting trust boundaries
The whole point of A2UI is that the client controls rendering. Do not rebuild the same security problems by letting the server smuggle unsafe behavior through custom components.
5. Overusing A2UI for things plain React already solves
If the UI is fully first-party and your app already knows exactly what should render, normal frontend code is often simpler. A2UI becomes valuable when the agent truly needs to decide the surface.
The Practical Adoption Path I Recommend
If I were introducing A2UI into a real product this quarter, I would do it in this order:
- Build one local React prototype with
A2UIViewer - Move to
A2UIProvider+A2UIRendererand add one meaningful action - Put message generation behind one backend endpoint
- Keep protocol versioning isolated in a server-side adapter
- Add custom components only after the core flow works
- Integrate a transport such as A2A, AG-UI, or MCP once the product workflow is validated
That sequence gets you real confidence quickly without turning the first experiment into an infrastructure project.
Final Take
A2UI is not exciting because “AI can generate widgets.” It is exciting because it gives us a clean contract for something the industry badly needs: agents that can return real, native-feeling work surfaces instead of only paragraphs.
The strongest signal for me is not the protocol alone. It is the combination of:
- official production usage notes
- official sample apps
- renderer libraries you can run now
- transport guidance for A2A, AG-UI, and MCP
That is what makes A2UI feel worth learning now instead of “interesting someday.”
If you only remember one thing from this article, make it this:
Use chat for intent. Use A2UI when the answer should actually be a UI.