One thing the assistant did, such as searching or running a query: its name, where it is (waiting, running, needs approval, done, failed), how long it took, and on request what went in and out.
Bring your own model.
These components only show what you give them, in one message shape, and report what the person does. They never call a model or a server, so they work with any backend. They are built for how assistive technology handles streaming text, tool steps and approvals.
import { ToolCall, type ToolPart, type ToolState } from "@rdloom/react";
const states: ToolState[] = ["pending", "running", "awaiting-approval", "approved", "denied", "done", "failed"];
const tool = (state: ToolState): ToolPart => ({
type: "tool",
id: state,
name: "query_sales",
title: "Searching your sales data",
state,
startedAt: "2026-03-01T10:00:00Z",
endedAt: state === "done" ? "2026-03-01T10:00:03Z" : undefined,
error: state === "failed" ? "The database did not answer in time." : undefined,
approval: state === "awaiting-approval" ? { summary: "Read every order from the last 12 months", risk: "low" } : undefined,
});
// Every state has its own icon and its own words: nothing depends on color alone.
export default function ToolCallStatesExample() {
return (
<div className="flex w-[30rem] max-w-full flex-col gap-2">
{states.map((state) => (
<ToolCall key={state} tool={tool(state)} />
))}
</div>
);
}Installation
npx rdloom add tool-callCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/tool-call.jsonWorks, but without upgrade tracking.
Usage
import { ToolCall, type ToolPart, type ToolState } from "@rdloom/react";
<div className="flex w-[30rem] max-w-full flex-col gap-2">
{states.map((state) => (
<ToolCall key={state} tool={tool(state)} />
))}
</div>With details
{
"from": "2026-02-01",
"to": "2026-02-28",
"groupBy": "week"
}{
"rows": 4,
"total": 48210
}import { ToolCall } from "@rdloom/react";
export default function ToolCallWithDetailsExample() {
return (
<div className="w-[30rem] max-w-full">
<ToolCall
defaultExpanded
tool={{
type: "tool",
id: "sales",
name: "query_sales",
title: "Searching your sales data",
state: "done",
startedAt: "2026-03-01T10:00:00Z",
endedAt: "2026-03-01T10:00:02.400Z",
input: { from: "2026-02-01", to: "2026-02-28", groupBy: "week" },
output: { rows: 4, total: 48210 },
}}
/>
</div>
);
}Needs approval
import { useState } from "react";
import { ToolCall, updateTool, type ChatMessage } from "@rdloom/react";
// You own the state. Approving or declining moves the tool forward; focus returns to the tool.
export default function ToolCallNeedsApprovalExample() {
const [message, setMessage] = useState<ChatMessage>({
id: "m",
role: "assistant",
parts: [
{
type: "tool",
id: "t",
name: "delete_drafts",
title: "Delete draft invoices",
state: "awaiting-approval",
approval: { summary: "Delete 12 draft invoices", risk: "high", reversible: false },
},
],
});
const tool = message.parts[0];
return (
<div className="w-[30rem] max-w-full">
{tool.type === "tool" && (
<ToolCall
tool={tool}
onApprove={(id) => setMessage((m) => updateTool(m, id, { state: "approved" }))}
onDeny={(id) => setMessage((m) => updateTool(m, id, { state: "denied" }))}
/>
)}
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
toolrequiredThe tool part from the message. | ToolPart | none |
onApproveCalled with the tool's id when the person approves it. | (toolId: string) => void | none |
onDenyCalled with the tool's id when the person declines it. | (toolId: string) => void | none |
defaultExpandedShow the input and result at first. | boolean | false |
focusApprovalMove focus to the approval box when this tool asks for approval. | boolean | false |
Accessibility
Role group, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Enter / Space on the header: shows or hides the input and result
- Tab: moves through the header, the approval buttons and the details
Screen readers announce
- The header reads as a button, e.g. "Searching your sales data, Running, collapsed"
- A change is announced once: "Searching your sales data: Done"
- A failed tool also reads its error message
What your code must do
- State is said in words ("Running", "Done", "Needs your approval") and drawn with a different icon for each, never color alone
- State changes are announced politely; a tool that arrives already finished is not read out again
- The header is a real button with aria-expanded, and only when there is something to show
- Input and result are scrollable regions that can be focused
- Once an approval is answered, focus returns to the tool's header instead of being lost
- Odd or huge values never throw: long output is cut with a count of what was left out
Guidelines
Use it when
- Showing the steps an assistant took to get to an answer
Avoid it when
- A progress bar for a long job: use Progress
- Steps a person walks through: use Steps
Don't
- Showing only a spinner with no words
- Hiding an approval inside a collapsed section
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-surface-default--rd-color-surface-subtle--rd-color-border-default--rd-color-border-strong--rd-color-text-default--rd-color-text-muted--rd-color-action-primary--rd-color-feedback-success--rd-color-feedback-danger--rd-color-feedback-warning--rd-color-feedback-info--rd-color-focus-ring--rd-radius-control