Elements · Reasoning
Guardrail notice
A refusal in its own shape, with the nearest thing it can do instead.
This asks for a working attack against infrastructure you don't own. I can help with the defensive side of the same problem.
Installation
npx shadcn@latest add "@assistant-ui/elements-guardrail-notice"First time? Set up a runtime
Runtime components read their state from an assistant-ui runtime. Add one to an existing project:
npx assistant-ui@latest initThen wrap your app in a runtime provider:
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
export default function App() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({ api: "/api/chat" }),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your components */}
</AssistantRuntimeProvider>
);
}The installation guide covers new projects, templates, and API routes.
npx shadcn@latest add "@assistant-ui/elements-guardrail-notice"Props-driven: no runtime or provider required.
An icon, a title, a short policy tag, an explanation, and an optional list of alternatives the reader can pick instead. Unlike a generic failure banner (see /elements/error-state), this is specifically for a policy stop rather than a transport or server error. With a runtime you detect the stop from the message's own status; standalone you decide when to show it.
Getting started
A refusal is not a separate concept from a normal message: it is an assistant message whose status finished as "incomplete" with reason "content-filter". Everything else on the notice, the title, the explanation, and the alternatives, is content your app supplies; the runtime only tells you that the stop happened.
Detect a content filter stop
"use client";
import { useAuiState } from "@assistant-ui/react";
function useIsGuardrailStop() {
return useAuiState((s) => {
const status = s.message.status;
return status?.type === "incomplete" && status.reason === "content-filter";
});
}Send an alternative back
import { useAui } from "@assistant-ui/react";
import { GuardrailNotice } from "@/components/assistant-ui/elements/guardrail-notice";
const ALTERNATIVES = ["Explain the defensive side of the same problem"];
function AssistantGuardrail() {
const isGuardrailStop = useIsGuardrailStop();
const aui = useAui();
if (!isGuardrailStop) return null;
return (
<GuardrailNotice
title="I can't help with that"
explanation="This asks for something I can't assist with directly."
policy="content-filter"
alternatives={ALTERNATIVES}
onPick={(alternative) => aui.thread.append(alternative)}
/>
);
}aui.thread.append(text) sends text as a new user message and starts a run with it, the same as if the reader had typed and sent it themselves.
Standalone, showing the notice at all, and what it says, is entirely your call.
Hold the refusal state
"use client";
import { GuardrailNotice } from "@/components/assistant-ui/elements/guardrail-notice";
export function RefusalNotice() {
return (
<GuardrailNotice
title="I can't help with that"
explanation="This asks for a working exploit against infrastructure you don't own."
policy="policy"
alternatives={["Explain how to defend against this instead"]}
/>
);
}Wire onPick to your own retry
<GuardrailNotice
/* ... */
onPick={(alternative) => sendMessage(alternative)}
/>onPick is optional; omitting it still renders the alternative buttons, they just do nothing on click.
Anatomy
<div data-slot="guardrail-notice">
<div>{/* shield icon, title, policy tag */}</div>
<p>{/* explanation, always rendered */}</p>
<div>{/* "try instead" label + one button per alternative, only when alternatives.length > 0 */}</div>
</div>The alternatives block is entirely omitted, not just empty, when alternatives is an empty array; the notice then ends after the explanation paragraph. Every alternative renders as its own button regardless of whether onPick is provided.
Examples
Reading the refusal reason
status.reason on an incomplete message is one of a fixed set of strings; "content-filter" is the one that maps to a guardrail stop specifically, distinct from "cancelled", "length", "error", or "other".
const policy = status.type === "incomplete" ? status.reason : undefined;Restyle the notice
Both lanes take className on the root, which starts as flex w-full max-w-sm flex-col gap-3 rounded-[20px] on the shared paper surface. The shield icon's amber tint and the policy tag's mono styling both come from surfaces.tsx.
<GuardrailNotice className="max-w-none rounded-2xl" /* ... */ />API reference
Message state
| Selector / call | Type | Description |
|---|---|---|
s.message.status | MessageStatus | undefined | type === "incomplete" with reason === "content-filter" marks a guardrail stop. |
aui.thread.append(message) | (message: CreateAppendMessage) => void | Sends a plain string (or a full message) as a new user turn; use it to send a picked alternative back. |
GuardrailNotice
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Headline next to the shield icon. |
explanation | string | required | Body text; always rendered. |
policy | string | required | Short tag shown at the end of the header row. |
alternatives | readonly string[] | required | Suggested next prompts. An empty array omits the whole block. |
onPick | (alternative: string) => void | Called with the clicked alternative's text. Optional; omitting it leaves the buttons inert. | |
className | string | Merged onto the root. |
All other div props except children, title, explanation, policy, alternatives, and onPick are forwarded to the root.