v1.5

Chat Tool

Collapsible card that shows an agent's tool call, its input, result, errors and approval step.

Pro
Completed
{
  "city": "Lisbon",
  "units": "metric"
}
{
  "temperature": 21,
  "condition": "Sunny",
  "humidity": 0.48
}

Description

ChatTool renders one tool call inside an assistant message. It is a compound component built on Radix Collapsible: a trigger row with a status glyph and the tool name, and a panel holding the input, the result or error, an optional approval step and the call id. Give it toolName, state, input and output and it builds every part for you. Pass children instead and you arrange the slots yourself.

The state prop takes the AI SDK ToolUIPart lifecycle names (input-streaming through output-denied), so part.state goes straight in. In-flight states show a spinner and shimmer the label, attention states tint the frame, and a card with nothing to reveal collapses to a static row. ChatToolGroup folds a batch of calls behind one trigger.

Use it wherever an agent runs functions in a chat: search, retrieval, API calls, file edits, and any action a user has to approve first. Don't use it for the model's reasoning steps. That is Chain of Thought. For a plain "thinking" indicator with no tool attached use Chat Loader, and for files the user attached to a prompt use Chat Attachment.

ChatTool is a Create UI Pro component. Install it with a Pro seat.

Installation

pnpm dlx @create-ui/cli add chat-tool

Anatomy

<ChatTool>
  <ChatTool.Trigger>
    <ChatTool.StatusIcon />
  </ChatTool.Trigger>
  <ChatTool.Content>
    <ChatTool.Args />
    <ChatTool.Result />
    <ChatTool.Error />
    <ChatTool.Denied />
    <ChatTool.Approval>
      <ChatTool.ApprovalActions>
        <ChatTool.Reject />
        <ChatTool.Approve />
      </ChatTool.ApprovalActions>
    </ChatTool.Approval>
    <ChatTool.Meta />
  </ChatTool.Content>
</ChatTool>
 
<ChatToolGroup>
  <ChatToolGroup.Trigger />
  <ChatToolGroup.Content>
    <ChatTool />
  </ChatToolGroup.Content>
</ChatToolGroup>

Usage

import { ChatTool } from "@/components/ui/chat-tool"
<ChatTool
  toolName="getWeather"
  state="output-available"
  input={{ city: "Lisbon" }}
  output={{ temperature: 21 }}
/>

Examples

Every example is a Pro preview (marked with a Pro badge).

States

Each lifecycle state has its own look. input-streaming, input-available and approval-responded show a neutral spinner and shimmer the label. approval-requested tints the frame with border-warning-weak, output-error with border-error-weak, and output-denied stays neutral with a forbid glyph. Leave state out and the card renders with no status glyph at all.

Pro
Preparing
Running
Waiting for approval
Running
Completed
Failed
Denied

Streaming input

argsText shows raw text verbatim and wins over input, so partial JSON renders while the model is still writing it. Switch to output once the call resolves. The label keeps the same element through every state, so the shimmer just stops.

Pro
Preparing
Preparing tool: listCommits

Approval

In approval-requested the preset renders Reject and Approve buttons for whichever of onReject and onApprove you pass. Approve is a solid primary button and Reject a neutral outline. Move the card to approval-responded while the agent picks up the answer, then to output-available or output-denied with a denialReason.

Pro
Waiting for approval
{
  "to": "[email protected]",
  "subject": "Launch moved to Thursday"
}

Errors

errorText replaces the result in output-error. toolCallId adds a monospace footer, handy when the error needs to be matched against provider logs. An output-error card without errorText renders no empty message row.

Pro
Failed
{
  "url": "https://status.acme.com/api/incidents"
}
Request timed out after 30s. The host closed the connection before sending any headers.
call_9f2c1a7b4e2d4c8fa10b3e7d

Composition

Pass children to skip the preset. ChatTool.StatusIcon still reads state from the root, and a glyph passed as its child replaces the built-in one while keeping the size and tone. ChatTool.Result takes children for a custom result body.

Pro
Completed
Parameters
{
  "city": "Lisbon"
}
Forecast
21°CSunny, humidity 48%

Group

ChatToolGroup collapses several calls behind one trigger. Set active while any call in the batch is still running so the group label shimmers too.

Pro
Completed
Running
Preparing

Controlled

open and onOpenChange control the panel from outside, for example to open every card when the user expands a whole conversation. defaultOpen sets the initial state when you don't need control.

Pro
Completed

Accessibility

When the card has something to reveal, the trigger is a native <button> from Radix Collapsible. Without a body it renders a plain <div>, so there is no dead button in the tab order.

KeyDescription
TabMoves focus to the trigger, then into the approval buttons.
Shift + TabMoves focus back to the previous control.
Space / EnterOpens or closes the panel on the trigger, or activates Approve and Reject.

ARIA notes:

  • The trigger manages aria-expanded and aria-controls through Radix. A static row sets neither.
  • The status glyph, the spinner and the chevron are decorative and marked aria-hidden, so the state has to be in the label text. Use triggerPrefix ("Failed tool:", "Approval needed:") or say it in your own trigger children.
  • The shimmer paints the text with a gradient. Under prefers-reduced-motion and forced colors it stops and the label renders as plain text. The spinner freezes and the panel stops animating under reduced motion too.
  • With several approval cards on screen, the default "Approve" and "Reject" names repeat. Pass approveLabel and rejectLabel that name the action ("Send email") so each button is distinct.
  • See the Radix Collapsible docs for the full disclosure behavior.

Styling

Tailwind override: className on the root and on every part is merged via cn().

<ChatTool
  className="rounded-component-lg"
  toolName="searchDocs"
  state="output-available"
/>

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="chat-tool" on the root, with data-tool-state (the lifecycle state), data-active ("true" / "false"), data-expandable ("true" / "false") and Radix's data-state ("open" / "closed").
  • data-slot="chat-tool-trigger" on the trigger row, with data-expandable and, when it is a button, data-state.
  • data-slot="chat-tool-trigger-label" on the label row, which holds the status glyph and a data-slot="text-shimmer" span with data-active.
  • data-slot="chat-tool-trigger-prefix" and data-slot="chat-tool-name" on the preset prefix and tool name.
  • data-slot="chat-tool-status" on the status wrapper, with data-tool-state. Inside it, data-slot="chat-tool-status-icon" on the glyph, or the spinner's own data-slot="spinner" while in flight.
  • data-slot="chat-tool-indicator" on the chevron.
  • data-slot="chat-tool-content" on the collapsible panel and data-slot="chat-tool-content-body" on its padded inner column.
  • data-slot="chat-tool-args", -args-label and -args-code on the input block.
  • data-slot="chat-tool-result", -result-label and -result-code on the result block.
  • data-slot="chat-tool-error" and -error-label on the error message.
  • data-slot="chat-tool-denied" and -denied-label on the denial reason.
  • data-slot="chat-tool-approval", -approval-actions, -approve and -reject on the approval step.
  • data-slot="chat-tool-meta" on the call id footer.
  • data-slot="chat-tool-group" on the group root, with data-active and data-state. data-slot="chat-tool-group-trigger", -group-trigger-label, -group-indicator, -group-content and -group-content-body on its parts.
[data-slot="chat-tool"][data-tool-state="output-denied"]
  [data-slot="chat-tool-name"] {
  /* ... */
}

States: only the frame and the status glyph change between states.

StateFrameGlyphLabel
no stateborder-lightnoneplain
input-streamingborder-lightneutral-soft spinnershimmers
input-availableborder-lightneutral-soft spinnershimmers
approval-requestedborder-warning-weakwarning, text-warning-baseplain
approval-respondedborder-lightneutral-soft spinnershimmers
output-availableborder-lightcheck, text-success-baseplain
output-errorborder-error-weakclose, text-error-baseplain
output-deniedborder-lightforbid, text-bodyplain

Motion: the panel uses animate-collapsible-down and animate-collapsible-up, and the chevron rotates over 200ms. Both are skipped under prefers-reduced-motion.

  • Chain of Thought: use it for the model's reasoning steps, not for function calls.
  • Chat Loader: use it for a waiting indicator when there is no tool call to show.
  • Accordion: use it for generic expandable sections outside a chat.
  • Chat Source: use it to cite the pages a retrieval tool returned inside the answer text.

API Reference

ChatTool

The root card. Wraps Radix's Collapsible.Root, so open, defaultOpen, onOpenChange, disabled and every div attribute pass through. Without children it builds the preset from the props below. Also exported as ChatToolRoot and ChatTool.Root.

Props

PropTypeDefaultDescription
stateChatToolState-Lifecycle state. Omit it for a neutral card with no status glyph.
toolNamestring-Tool name shown in the trigger, in medium weight.
triggerPrefixReact.ReactNode-Text before the tool name, such as "Used tool:".
inputunknown-Tool input, serialised to JSON.
argsTextstring-Raw input text shown verbatim. Wins over input, and suits partial JSON.
outputunknown-Tool output, serialised to JSON. Hidden in output-error and output-denied.
errorTextstring-Error message shown in the panel.
denialReasonstring-Reason shown in output-denied.
onApprove() => void-Renders the Approve button in approval-requested.
onReject() => void-Renders the Reject button in approval-requested.
approveLabelReact.ReactNode"Approve"Label of the Approve button.
rejectLabelReact.ReactNode"Reject"Label of the Reject button.
toolCallIdstring-Call id shown as a monospace footer.
activebooleanin-flight statesForces the label shimmer on or off.
isExpandablebooleanhas a body or childrenForces the trigger to a button (true) or a static row (false).
openboolean-Controlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Fires when the panel opens or closes.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Custom parts. When set, the preset props that build parts are ignored.

Variants

VariantOptionsDefaultDescription
state"input-streaming" "input-available" "approval-requested" "approval-responded" "output-available" "output-error" "output-denied"-Drives the glyph, the frame tint, the shimmer and which slots show.

ChatTool.Trigger

The header row. Wraps Radix's Collapsible.Trigger (a <button>) when the card is expandable and renders a static <div> otherwise. ChatTool.StatusIcon children stay outside the shimmer; every other child becomes the truncating label text.

Props

PropTypeDefaultDescription
asChildbooleanfalseRadix asChild, only used when expandable.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-A ChatTool.StatusIcon and the label text.

ChatTool.StatusIcon

The status glyph. Extends React.ComponentProps<"span">. Reads state from the root and renders nothing when there is neither a state nor a custom glyph.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged onto the wrapper via cn().
childrenReact.ReactElement-Custom glyph. Gets the size and state tone; its own className wins.

ChatTool.Content

The collapsible panel. Wraps Radix's Collapsible.Content and renders nothing on a static card.

Props

PropTypeDefaultDescription
forceMounttrue-Radix forceMount, keeps the panel in the DOM.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Payload slots.

ChatTool.Args

The input block. Extends React.ComponentProps<"div"> and renders nothing when there is no text and no children.

Props

PropTypeDefaultDescription
argsTextstring-Raw text, shown verbatim. Wins over input.
inputunknown-Structured input, serialised to JSON.
labelReact.ReactNode-Uppercase caption above the block.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Replaces the code block.

ChatTool.Result

The output block. Extends React.ComponentProps<"div">. Hidden in output-error and output-denied, and when there is no value and no children.

Props

PropTypeDefaultDescription
valueunknown-Output, serialised to JSON. Strings pass through.
labelReact.ReactNode-Uppercase caption above the block.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Replaces the code block.

ChatTool.Error

The error message in text-error-base. Extends React.ComponentProps<"div"> and renders nothing without errorText or children.

Props

PropTypeDefaultDescription
errorTextstring-Error message.
labelReact.ReactNode-Uppercase caption above the message.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Replaces errorText.

ChatTool.Denied

The denial reason in text-body. Extends React.ComponentProps<"div"> and renders nothing without reason or children.

Props

PropTypeDefaultDescription
reasonstring-Why the call was denied.
labelReact.ReactNode-Uppercase caption above the message.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Replaces reason.

ChatTool.Approval / ChatTool.ApprovalActions

Approval is the approval step and renders only in approval-requested. ApprovalActions is the right-aligned button row inside it. Both extend React.ComponentProps<"div">.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Content or buttons.

ChatTool.Approve / ChatTool.Reject

The approval buttons. Both render the registry Button at size="sm" and accept every Button prop, which override the defaults below.

Props

PropTypeDefaultDescription
variantButtonProps["variant"]"primary" / "neutral-light"Approve / Reject color.
appearanceButtonProps["appearance"]"solid" / "outline"Approve / Reject fill.
onClickReact.MouseEventHandler-Click handler.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Button label.

ChatTool.Meta

The monospace call id footer. Extends React.ComponentProps<"div"> and renders nothing without toolCallId or children.

Props

PropTypeDefaultDescription
toolCallIdstring-Provider call id.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Replaces toolCallId.

ChatToolGroup

Collapses a batch of calls. Wraps Radix's Collapsible.Root, so open, defaultOpen, onOpenChange and disabled pass through. Parts: ChatToolGroup.Trigger (a Radix Collapsible.Trigger with a truncating label and chevron) and ChatToolGroup.Content (a Radix Collapsible.Content with a padded column). Also exported as ChatToolGroupRoot, ChatToolGroupTrigger and ChatToolGroupContent.

Props

PropTypeDefaultDescription
activebooleanfalseShimmers the group label while calls run.
openboolean-Controlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Fires when the group opens or closes.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-ChatToolGroup.Trigger and ChatToolGroup.Content.

Types

type ChatToolState =
  | "input-streaming"
  | "input-available"
  | "approval-requested"
  | "approval-responded"
  | "output-available"
  | "output-error"
  | "output-denied"

The file also exports the CVA recipes chatToolVariants, chatToolTriggerVariants, chatToolContentVariants, chatToolCodeVariants and chatToolGroupVariants. None of them take variant options.