Native generative UI

Let an agent render native Angular components, in the chat and in areas of your page.

Open your coding agent in your project's folder, or in an empty folder for a new app.This runs in a coding agent on your computer.

An agent that answers only in text has to describe a product, a chart or a form. With native generative UI it renders them: it sends A2UI operations for a catalog of components, and @copilotkit/angular/generative-ui renders them with Angular components, in the chat and in named areas of your page. The catalog, the engine and the prompts are the same as in React; the components are native Angular.

Experimental. The API can still change between minor versions.

Turn it on#

Add provideGenerativeUI next to provideCopilotKit, at the root. The chat resolves its renderers from the root injector, so a provider in a lazy route is never seen. standardUI is the standard catalog: 69 components for layout, text, inputs, feedback, data, media and composites.

src/app/app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideCopilotKit } from "@copilotkit/angular";
import {
  provideGenerativeUI,
  standardUI,
} from "@copilotkit/angular/generative-ui";

export const appConfig: ApplicationConfig = {
  providers: [
    provideCopilotKit({ runtimeUrl: "/api/copilotkit" }),
    provideGenerativeUI({ ui: standardUI }),
  ],
};

The runtime needs no change. When the provider sends the catalog, the runtime gives that run the render_ui tool and streams each surface to the client. Set generativeUI: true on CopilotRuntime only to add the tool to every run. See Transports for the markdown transport, which never uses the tool.

Three standard components draw with optional libraries. Each has its own entry point, so the library stays out of apps that do not use it. Without the provider, the component shows a fallback: a data table for Chart, a list of places for Map, nothing for Icon. Install the library of each provider you add:

npm install chart.js maplibre-gl lucide
src/app/app.config.ts
import { provideGenerativeUIChart } from "@copilotkit/angular/generative-ui/chart"; // chart.js
import { provideGenerativeUIMap } from "@copilotkit/angular/generative-ui/map"; // maplibre-gl
import { provideGenerativeUIIcons } from "@copilotkit/angular/generative-ui/icons"; // lucide

providers: [
  provideGenerativeUI({ ui: standardUI }),
  provideGenerativeUIChart(),
  provideGenerativeUIMap(),
  provideGenerativeUIIcons(),
];

Your own components#

Declare a catalog with createGenerativeUI, once, at module level. A component of the catalog is a standalone Angular component whose inputs are the keys of its props schema. The engine creates it, binds the parsed props to the inputs it declares, and keeps them current.

src/app/shop-ui.ts
import { Component, input, model } from "@angular/core";
import { z } from "zod";
import {
  CopilotGenerativeUIChildren,
  child,
  createGenerativeUI,
  injectGenerativeUIAccessibility,
  standardUI,
  type GenerativeUIChildren,
  type GenerativeUIEventHandler,
} from "@copilotkit/angular/generative-ui";

@Component({
  selector: "app-product-card",
  imports: [CopilotGenerativeUIChildren],
  host: { role: "article", "[attr.aria-label]": "aria()['aria-label'] ?? null" },
  template: `
    <h3>{{ name() }}</h3>
    <p>{{ "$" + price() }}</p>
    <input type="number" [value]="quantity() ?? 1"
      (input)="quantity.set(+$any($event.target).value)" />
    @if (onAddToCart(); as addToCart) {
      <button (click)="addToCart({ productId: productId(), quantity: quantity() ?? 1 })">
        Add to cart
      </button>
    }
    <ng-container *copilotGenerativeUIChildren="children()" />
  `,
})
export class ProductCard {
  readonly productId = input.required<string>();
  readonly name = input.required<string>();
  readonly price = input.required<number>();
  /** A writable prop: a `model()`. The engine binds it two-way. */
  readonly quantity = model<number>();
  /** An event: a function input. It is set only when the event is bound. */
  readonly onAddToCart =
    input<GenerativeUIEventHandler<typeof shopUI, "onAddToCart">>();
  /** A slot: the children the agent puts in the card. */
  readonly children = input<GenerativeUIChildren>();
  protected readonly aria = injectGenerativeUIAccessibility();
}

export const shopUI = createGenerativeUI({
  id: "acme/shop@1",
  extends: [standardUI],
  state: {
    cart: z.object({ count: z.number(), total: z.number() }),
  },
  functions: {
    addToCart: {
      parameters: z.object({ productId: z.string(), quantity: z.number() }),
    },
  },
  components: {
    ProductCard: {
      component: ProductCard,
      description: "One product with its price and an add-to-cart button.",
      props: z.object({
        productId: z.string(),
        name: z.string(),
        price: z.number(),
        quantity: z.number().optional(),
      }),
      writable: ["quantity"],
      // Matches the `addToCart` function, so it calls it with its parameters.
      events: { onAddToCart: {} },
      slots: { children: "multiple" },
    },
  },
});

How the parts of a definition reach the component:

In the catalogIn the component
A key of propsinput() with that name. The value is the parsed schema output.
A key in writablemodel() with that name. A write goes to the surface data model when the agent bound the prop with { "@path" }, else it stays local to the node.
A key of eventsinput<GenerativeUIEventHandler<...>>() with the event name. It holds a function only when the event is bound, so @if on it shows the button only then. An event whose name matches a function, like onAddToCart and addToCart, is always bound, calls the function, and can leave out parameters.
A slot, or a child() / childList() in propsinput<GenerativeUIChildren>(). Render it with *copilotGenerativeUIChildren. A child() inside a list arrives inside the parsed value, for example tabs()[i].child.
The accessibility envelopeinjectGenerativeUIAccessibility() gives the ARIA attributes to bind on the root element. Give that element a role, such as article, so a screen reader announces the label.

The host element is the one of the selector (div[copilotGenerativeUIMap]). A component whose tag depends on the node, the way a React component picks h2 or h3 at render time, declares static hostTag(props, injector) and returns the tag; the renderer creates that element as the host and creates the component again when the tag changes. The standard Title, LinkCard and TableCell use it.

GenerativeUIComponentInputs<typeof shopUI, "ProductCard"> is the type of those values, for a component that wants one type for all of them. The rules of a catalog (bound props, bind selectors, overrides, omit) are in Catalogs; only the component code differs from React.

Angular has no per-component error boundary. A component that throws while it is created shows the fallback and reports to onError; an error in a later change detection goes to the ErrorHandler of the app.

Show live app state and run your code#

connectGenerativeUIState gives one key of the catalog's state its value, from a signal or a function, and keeps it current. registerGenerativeUIFunction mounts the handler of a declared function. Both live as long as the injection context they are called in.

src/app/shop.component.ts
import { Component, inject } from "@angular/core";
import {
  connectGenerativeUIState,
  registerGenerativeUIFunction,
} from "@copilotkit/angular/generative-ui";
import { CartStore } from "./cart.store";
import { shopUI } from "./shop-ui";

@Component({ /* ... */ })
export class ShopComponent {
  readonly #cart = inject(CartStore);

  constructor() {
    connectGenerativeUIState(shopUI, "cart", () => ({
      count: this.#cart.count(),
      total: this.#cart.total(),
    }));
    registerGenerativeUIFunction(shopUI, "addToCart", ({ productId, quantity }) =>
      this.#cart.add(productId, quantity),
    );
  }
}

The ideas (bound props, who may call a function, events that go to the agent as a user message) are in State and actions.

Render in areas of your page#

A surface renders in the chat by default. <copilot-generative-ui-surface> is a named target on the page that the agent can render into. Its content is the fallback until a surface arrives.

<copilot-generative-ui-surface
  id="recommendations"
  description="Next to the product list. Use it for product recommendations."
  [ui]="shopUI"
  [components]="['ProductCard', 'Row']"
>
  <app-featured-products />
</copilot-generative-ui-surface>

components limits what the agent can render there; autoScroll (default true) scrolls the page to the surface when a generation starts. See Surfaces.

Markdown transport#

With transport: "markdown", the agent writes the operations in a2ui code blocks inside its reply, and the client renders them. No tool, nothing on the server. provideGenerativeUI then renders each assistant message with the blocks replaced by their surfaces, unless the app gives <copilot-chat-assistant-message> its own markdown renderer.

provideGenerativeUI({ ui: standardUI, transport: "markdown" });

See Markdown mode for the prompt and the limits.

Theme, chat card and errors#

provideGenerativeUI({
  ui: standardUI,
  // The `--a2ui-*` variables. A signal makes it reactive.
  theme: { "--a2ui-color-accent": "#0ea5e9", dark: { "--a2ui-color-accent": "#38bdf8" } },
  // Replaces the card around a surface in the chat (inputs: surface, surfaceId,
  // isStreaming, error, rejected). Render the surface with
  // <copilot-generative-ui-surface-view [surface]>.
  chatCard: MyChatCard,
  // Replaces the per-component fallback (inputs: message, componentId, and
  // retry after a render error if the component declares it).
  errorFallback: MyFallback,
  onError: (info) => console.warn(info.kind, info.surfaceId, info.error),
});

The variables and dark mode are in Theming.

Differences from React#

  • Inputs instead of props: input() for a prop, model() for a writable prop (setValue in React is value.set() here), a function input for an event.
  • Slots and child() values are GenerativeUIChildren handles, rendered with *copilotGenerativeUIChildren, not rendered nodes.
  • One provideGenerativeUI for the app, at the root, instead of a prop on the provider.
  • connectGenerativeUIState and registerGenerativeUIFunction instead of the useGenerativeUIState and useGenerativeUIFunction hooks.
  • No error boundary around each component (see the note above).
  • A component is a host element, so a tag that depends on the node comes from static hostTag(props, injector) instead of a Tag variable in JSX.

Next steps#

  • Components and surfaces — the ideas, with the standard catalog, catalogs, state and actions, theming and transports.
  • Standard catalog — the 69 components and how to prompt for them.
  • A2UI — the lower-level A2UI renderer for agents that emit A2UI themselves.