Render client-tool states and fenced Agent responses with domain-specific React components.
Add a custom component when your product needs a domain-specific surface that is not part of the built-in Agent UI catalog. HeroUI Agent supports two client-side extension points:
| Extension point | Use it when |
|---|---|
Client tool render | Your component represents a declared browser tool's arguments, approval, progress, or result. |
| Custom Markdown renderer | Your component represents a model-authored fenced block in an otherwise normal response. |
Neither path registers a new generated Agent UI contract with the hosted runtime. Your components and implementation code stay in the browser; model-generated charts, tables, forms, and layouts still use the built-in validated catalog.
Add render to a tool created with createToolHelper. The callback runs for every tool state and can return your React component. Return null whenever you want the embed's default tool card instead.
import {createToolHelper} from "@heroui/agent";
import {z} from "zod";
type AccountHealth = {
account: string;
status: "at-risk" | "healthy";
};
type AppContext = {
getAccountHealth: (accountId: string) => Promise<AccountHealth>;
};
const tool = createToolHelper<AppContext>();
function AccountHealthCard({health}: {health: AccountHealth}) {
return (
<section className="rounded-xl border p-4">
<strong>{health.account}</strong>
<p className="mt-1 text-sm">Status: {health.status}</p>
</section>
);
}
export const accountTools = [
tool({
name: "get_account_health",
description: "Get the current health of an account",
parameters: z.object({accountId: z.string()}),
execute: ({accountId}, context) => context.getAccountHealth(accountId),
render: ({result, status}) => {
if (status !== "completed") return null;
return <AccountHealthCard health={result as AccountHealth} />;
},
}),
];render receives ClientToolRenderProps. Its status covers argument streaming, approval, execution, completion, rejection, and errors. The completed result is unknown, so narrow or validate it before passing it to your component.
Use a tool renderer when the custom UI must be tied to a real browser function or permission flow. See Client Tools for context, approval, and tool-state details.
Custom Markdown renderers map a fenced-code language to a React component. Use one for presentational content that does not need to execute a client tool.
Define the component outside your application component so React receives a stable component reference. The simplest renderer can treat the fenced block as plain text—no custom parser or schema is required.
import type {AgentMarkdownRendererProps} from "@heroui/agent";
export function SupportNoteRenderer({code, isIncomplete}: AgentMarkdownRendererProps) {
return (
<aside aria-busy={isIncomplete} className="rounded-xl border p-4">
<strong>Support note</strong>
<p className="mt-2 text-sm">{code || "Writing note…"}</p>
</aside>
);
}Add the component to markdown.plugins.renderers. One renderer can match one language or an array of languages.
"use client";
import type {AgentMarkdownRenderer} from "@heroui/agent";
import {HeroUIAgent} from "@heroui/agent";
import {SupportNoteRenderer} from "./support-note";
const renderers = [
{
component: SupportNoteRenderer,
language: "support-note",
},
] satisfies AgentMarkdownRenderer[];
export function AppAgent() {
return (
<HeroUIAgent
getAuthToken={getAuthToken}
markdown={{plugins: {renderers}}}
agentId={process.env.NEXT_PUBLIC_HEROUI_AGENT_ID!}
/>
);
}Describe the fence and its content contract in your system prompt. Keep the instruction narrow so ordinary answers remain ordinary Markdown.
When a support answer has one important next step, finish with a fenced code
block labeled support-note. Put one short plain-text sentence inside the block.
Do not include Markdown or JSON. Use at most one support note per response.The matching response Markdown looks like this:
```support-note
Save your recovery codes before enabling two-factor authentication.
```AgentMarkdownRendererPropsImport AgentMarkdownRendererProps from @heroui/agent to type a custom renderer. Its public shape is:
type AgentMarkdownRendererProps = {
code: string;
isIncomplete: boolean;
language: string;
meta?: string;
};| Prop | Type | Description |
|---|---|---|
code | string | The current contents of the fenced code block. |
language | string | The language identifier that selected the renderer. |
meta | string? | Everything after the language identifier in the opening fence. |
isIncomplete | boolean | true while the response is streaming and the payload may still be incomplete. |
code as untrusted, model-generated content. Render it as text rather than generated HTML.isIncomplete to keep the component understandable while the response is streaming.dangerouslySetInnerHTML.For the complete markdown configuration surface, see Configuration.