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#

PathBest fitVue setup
A component the agent showsYou want the agent to display a component and nothing else runsuseComponent
Your componentsThe agent already owns the tool and you draw its calluseRenderTool, or useFrontendTool with a render
Default cardYou want something better than raw JSON, with no per-tool workuseDefaultRenderTool()
A2UIThe agent emits declarative A2UI operations:a2ui on CopilotKitProvider
Open Generative UIThe agent generates HTML/CSS to be sandboxed:open-generative-ui on CopilotKitProvider
MCP AppsAn MCP server returns an interactive app resourceNothing — 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.

src/IncidentCard.vue
<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>
src/App.vue
<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.

src/TriageCard.vue
<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:

src/App.vue
<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 / useDefaultRenderTooluseFrontendTool's render
Argumentsparametersargs
Status&quot;inProgress&quot; \</td><td>&quot;executing&quot; \</td><td>&quot;complete&quot;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:

server.ts
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#