Generative UI in Vue
Render an agent's tool calls, A2UI surfaces, and sandboxed UI as real Vue components instead of plain text.
Generative UI is how a tool result becomes a rendered surface — a card, a
badge, a form — instead of a paragraph of text in the chat log. Everything on
this page is Vue-native: @copilotkit/vue ships its own renderers and does
not depend on the React packages.
This does not depend on your agent framework
Generative UI is a frontend concern. It reads AG-UI tool calls, and every
agent framework the runtime supports — LangGraph, CrewAI, Mastra, AWS
Strands, Agno, a BuiltInAgent, or a custom AbstractAgent — emits the same
tool calls. Nothing on this page changes when you swap the agent behind the
runtime. If a framework's page does not mention Vue rendering, that is
because rendering is not a framework-specific topic, not because the path is
missing.
Choose a path#
| Path | Best fit | Vue setup |
|---|---|---|
| A component the agent shows | You want the agent to display a component and nothing else runs | useComponent |
| Your components | The agent already owns the tool and you draw its call | useRenderTool, or useFrontendTool with a render |
| Default card | You want something better than raw JSON, with no per-tool work | useDefaultRenderTool() |
| A2UI | The agent emits declarative A2UI operations | :a2ui on CopilotKitProvider |
| Open Generative UI | The agent generates HTML/CSS to be sandboxed | :open-generative-ui on CopilotKitProvider |
| MCP Apps | An MCP server returns an interactive app resource | Nothing — the renderer is always registered |
All of these require the composable or component to sit inside
CopilotKitProvider. Import
everything from the /v2 subpath.
Let the agent display a component#
The shortest path, and the one that needs nothing on your agent.
useComponent registers a Vue component as a tool the
agent can call to show it. There is no handler and no user interaction: the agent decides
when, and fills the props from the schema you declare.
<script setup lang="ts">
defineProps<{ id: string; severity: string; summary: string }>();
</script>
<template>
<article class="incident-card">
<header>
<strong>{{ id }}</strong>
<span :data-severity="severity">{{ severity }}</span>
</header>
<p>{{ summary }}</p>
</article>
</template><script setup lang="ts">
import { z } from "zod";
import { CopilotChat, CopilotKitProvider, useComponent } from "@copilotkit/vue/v2";
import IncidentCard from "./IncidentCard.vue";
useComponent({
name: "show_incident",
description: "Show one incident from the incident table.",
parameters: z.object({
id: z.string().describe("The incident id, such as INC-4711"),
severity: z.string().describe("One of sev1, sev2, sev3"),
summary: z.string().describe("One sentence on what happened"),
}),
render: IncidentCard,
});
</script>The schema arrives at the component as its props, so the component stays an ordinary Vue component that knows nothing about CopilotKit. Nothing is added to the agent: the tool is declared here and forwarded over AG-UI, which is why this works the same behind a Python agent as a TypeScript one.
`useComponent` or `useRenderTool`?
Use useComponent when the component is the feature and the frontend declares the
tool. Use useRenderTool when your agent already
owns the tool and the browser only draws its call, which is the next section. The give-away
is where the tool lives: if removing the component would remove the tool from the agent's
list, it is a useComponent.
A well-formed component is not a correct one
The model fills these props from what it knows. A card rendered over records your
application does not hold looks the same in the browser, in a screenshot, and in a video as
a correct one. Share the page's data with the agent using
useAgentContext, then read the rendered fields
against the records you actually hold.
Render a server-side tool call#
When the tool runs on your server and the browser only draws the result, use
useRenderTool. Give it the tool name, a
schema for the arguments, and a renderer.
This is not JSX. The renderer is either a function returning a VNodeChild
(build it with Vue's h()) or a plain Vue component.
<script setup lang="ts">
defineProps<{
name: string;
toolCallId: string;
status: "inProgress" | "executing" | "complete";
parameters: { incidentId?: string; severity?: string };
result?: string;
}>();
</script>
<template>
<p v-if="status !== 'complete'">Triaging {{ parameters.incidentId }}…</p>
<article v-else class="triage-card">
<header>
<strong>{{ parameters.incidentId }}</strong>
<span :data-severity="parameters.severity">{{ parameters.severity }}</span>
</header>
<p>{{ result }}</p>
</article>
</template>Register it once, anywhere under the provider:
<script setup lang="ts">
import { z } from "zod";
import { CopilotChat, CopilotKitProvider, useRenderTool } from "@copilotkit/vue/v2";
import TriageCard from "./TriageCard.vue";
useRenderTool({
name: "display_triage",
parameters: z.object({
incidentId: z.string(),
severity: z.enum(["sev1", "sev2", "sev3"]),
}),
render: TriageCard,
});
</script>
<template>
<CopilotKitProvider runtime-url="http://localhost:8200/api/copilotkit">
<div style="height: 100vh"><CopilotChat /></div>
</CopilotKitProvider>
</template>The renderer runs three times as the call progresses. status moves
"inProgress" → "executing" → "complete", and parameters is a
Partial<T> while arguments are still streaming, so guard on status before
reading a field you require.
Pass reactive values through `deps`
useRenderTool takes a second argument of Vue WatchSources — refs or
getters, not a React-style dependency array. If your renderer closes over
reactive state, list it there or the registered renderer keeps the stale
value:
useRenderTool({ name: "display_triage", parameters, render }, [locale]);Give every other tool a card#
useDefaultRenderTool() registers a wildcard renderer for any tool without a
named one. Called with no arguments it installs CopilotKit's built-in
expandable card showing the tool name, status, arguments, and result — a
one-line upgrade from raw output.
<script setup lang="ts">
import { useDefaultRenderTool } from "@copilotkit/vue/v2";
useDefaultRenderTool();
</script>Pass { render } to substitute your own component. It is a thin wrapper around
useRenderTool({ name: "*" }), so the props are the same.
Render a tool that runs in the browser#
When the agent should call code in the page, use
useFrontendTool and give the same
registration a render.
<script setup lang="ts">
import { h } from "vue";
import { z } from "zod";
import { useFrontendTool } from "@copilotkit/vue/v2";
useFrontendTool({
name: "applyDashboardFilter",
description: "Filter the incident dashboard",
parameters: z.object({ severity: z.string() }),
handler: async ({ severity }, { signal }) => {
const response = await fetch(`/api/incidents?severity=${severity}`, { signal });
return response.text();
},
render: ({ args, status }) =>
h("div", `Filtering by ${args.severity ?? "…"} (${status})`),
});
</script>The two renderers do not receive the same props
This is the easiest thing to get wrong. useRenderTool normalizes its
props; useFrontendTool passes yours straight through to the core renderer.
useRenderTool / useDefaultRenderTool | useFrontendTool's render | |
|---|---|---|
| Arguments | parameters | args |
| Status | "inProgress" \</td><td>"executing" \</td><td>"complete" | the ToolCallStatus enum |
A renderer written for one will silently draw nothing in the other. If you want to share a component between them, wrap it rather than reusing it directly.
A2UI surfaces#
A2UI lets the agent describe a surface declaratively and
the frontend build it from a catalog of allowed components. The Vue renderer is
built on @a2ui/web_core and deliberately duplicates the small amount of
shared logic it needs, so enabling A2UI in a Vue app pulls in no React
dependencies.
Enable A2UI on the runtime:
const runtime = new CopilotRuntime({
agents: { default: myAgent },
a2ui: {},
});A bare a2ui: {} is enough to turn it on — the option is treated as enabled
whenever it is present, and only an explicit a2ui: { enabled: false } turns
it off.
The frontend needs nothing else — the built-in renderer activates on its own
once the runtime reports A2UI is configured. Pass :a2ui only to override
something:
<template>
<CopilotKitProvider runtime-url="/api/copilotkit" :a2ui="{}">
<div style="height: 100vh"><CopilotChat /></div>
</CopilotKitProvider>
</template>a2ui accepts theme, catalog, loadingComponent, and includeSchema.
Supplying your own catalog#
Passing a catalog does double duty: it selects the components the agent may
use, and it tells the runtime a catalog exists by forwarding
a2uiCatalogAvailable. That means A2UI switches on from the client side even
when the runtime has no a2ui configuration of its own.
<script setup lang="ts">
import { CopilotChat, CopilotKitProvider, vueBasicCatalog } from "@copilotkit/vue/v2";
</script>
<template>
<CopilotKitProvider
runtime-url="/api/copilotkit"
:a2ui="{ catalog: vueBasicCatalog }"
>
<div style="height: 100vh"><CopilotChat /></div>
</CopilotKitProvider>
</template>vueBasicCatalog is the built-in component set, exported directly from
@copilotkit/vue/v2. To have the agent compose your own design system instead,
build a catalog of your components and pass that — see
A2UI for the catalog format and the fixed- versus
dynamic-schema tradeoff.
Open Generative UI#
Open Generative UI lets the agent generate markup that renders inside a sandboxed iframe with no same-origin access. It can only reach back into your app through host functions you list explicitly.
<script setup lang="ts">
import { z } from "zod";
import { CopilotChat, CopilotKitProvider } from "@copilotkit/vue/v2";
const openGenerativeUI = {
sandboxFunctions: [
{
name: "setDashboardFilter",
description: "Set the active dashboard filter",
parameters: z.object({ filter: z.string() }),
handler: async ({ filter }: { filter: string }) => {
sessionStorage.setItem("dashboard-filter", filter);
return { applied: filter };
},
},
],
};
</script>
<template>
<CopilotKitProvider
runtime-url="/api/copilotkit"
:open-generative-ui="openGenerativeUI"
>
<div style="height: 100vh"><CopilotChat /></div>
</CopilotKitProvider>
</template>sandboxFunctions must be a stable array — define it outside the template, as
above, rather than inline. designSkill overrides the design guidance handed
to the generation tool.
MCP Apps#
Nothing to configure. The built-in MCP Apps renderer is always merged into the provider's activity renderers, so an interactive app resource returned by an MCP server renders in chat as soon as the agent surfaces it. See MCP Apps.
Verify it rendered#
A surface that silently falls back to text is the common failure. Two checks:
- Open the Inspector and confirm the tool call appears
with the status transitions above. A call that never reaches
"complete"means the tool failed server-side, not that the renderer is wrong. - Confirm the registered name matches the tool name exactly. A renderer registered under the wrong name is not an error — the call just falls through to the default renderer, or to text if you have not registered one.
Next steps#
useComponent— a component the agent calls to displayuseRenderTool— full render props and overloadsuseDefaultRenderTool— the wildcard rendereruseFrontendTool— browser-side toolsCopilotKitProvider—a2ui,openGenerativeUI, and renderer props- A2UI — the protocol, fixed and dynamic schemas