Tool
A collapsible component for displaying tool invocation details in AI chatbot interfaces.
The Tool component displays a collapsible interface for showing/hiding tool details. It is designed to take the ToolUIPart type from the AI SDK and display it in a collapsible interface.
"use client";import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput,} from "@/components/ai-elements/elements/tool";import type { ToolUIPart } from "ai";import { nanoid } from "nanoid";const toolCall: ToolUIPart = { type: "tool-database_query" as const, toolCallId: nanoid(), state: "output-available" as const, input: { query: "SELECT COUNT(*) FROM users WHERE created_at >= ?", params: ["2024-01-01"], database: "analytics", }, output: `| User ID | Name | Email | Created At ||---------|------|-------|------------|| 1 | John Doe | john@example.com | 2024-01-15 || 2 | Jane Smith | jane@example.com | 2024-01-20 || 3 | Bob Wilson | bob@example.com | 2024-02-01 || 4 | Alice Brown | alice@example.com | 2024-02-10 || 5 | Charlie Davis | charlie@example.com | 2024-02-15 |`, errorText: undefined,};const Example = () => ( <div style={{ height: "500px" }}> <Tool> <ToolHeader state={toolCall.state} type={toolCall.type} /> <ToolContent> <ToolInput input={toolCall.input} /> {toolCall.state === "output-available" && ( <ToolOutput errorText={toolCall.errorText} output={toolCall.output} /> )} </ToolContent> </Tool> </div>);export default Example;Installation
npx ai-elements@latest add toolnpx shadcn@latest add @ai-elements/tool"use client";import { Badge } from "@repo/shadcn-ui/components/ui/badge";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@repo/shadcn-ui/components/ui/collapsible";import { cn } from "@repo/shadcn-ui/lib/utils";import type { ToolUIPart } from "ai";import { CheckCircleIcon, ChevronDownIcon, CircleIcon, ClockIcon, WrenchIcon, XCircleIcon,} from "lucide-react";import type { ComponentProps, ReactNode } from "react";import { isValidElement } from "react";import { CodeBlock } from "./code-block";export type ToolProps = ComponentProps<typeof Collapsible>;export const Tool = ({ className, ...props }: ToolProps) => ( <Collapsible className={cn("not-prose mb-4 w-full rounded-md border", className)} {...props} />);export type ToolHeaderProps = { title?: string; type: ToolUIPart["type"]; state: ToolUIPart["state"]; className?: string;};const getStatusBadge = (status: ToolUIPart["state"]) => { const labels = { "input-streaming": "Pending", "input-available": "Running", "output-available": "Completed", "output-error": "Error", } as const; const icons = { "input-streaming": <CircleIcon className="size-4" />, "input-available": <ClockIcon className="size-4 animate-pulse" />, "output-available": <CheckCircleIcon className="size-4 text-green-600" />, "output-error": <XCircleIcon className="size-4 text-red-600" />, } as const; return ( <Badge className="gap-1.5 rounded-full text-xs" variant="secondary"> {icons[status]} {labels[status]} </Badge> );};export const ToolHeader = ({ className, title, type, state, ...props}: ToolHeaderProps) => ( <CollapsibleTrigger className={cn( "flex w-full items-center justify-between gap-4 p-3", className )} {...props} > <div className="flex items-center gap-2"> <WrenchIcon className="size-4 text-muted-foreground" /> <span className="font-medium text-sm"> {title ?? type.split("-").slice(1).join("-")} </span> {getStatusBadge(state)} </div> <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" /> </CollapsibleTrigger>);export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;export const ToolContent = ({ className, ...props }: ToolContentProps) => ( <CollapsibleContent className={cn( "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in", className )} {...props} />);export type ToolInputProps = ComponentProps<"div"> & { input: ToolUIPart["input"];};export const ToolInput = ({ className, input, ...props }: ToolInputProps) => ( <div className={cn("space-y-2 overflow-hidden p-4", className)} {...props}> <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide"> Parameters </h4> <div className="rounded-md bg-muted/50"> <CodeBlock code={JSON.stringify(input, null, 2)} language="json" /> </div> </div>);export type ToolOutputProps = ComponentProps<"div"> & { output: ToolUIPart["output"]; errorText: ToolUIPart["errorText"];};export const ToolOutput = ({ className, output, errorText, ...props}: ToolOutputProps) => { if (!(output || errorText)) { return null; } let Output = <div>{output as ReactNode}</div>; if (typeof output === "object" && !isValidElement(output)) { Output = ( <CodeBlock code={JSON.stringify(output, null, 2)} language="json" /> ); } else if (typeof output === "string") { Output = <CodeBlock code={output} language="json" />; } return ( <div className={cn("space-y-2 p-4", className)} {...props}> <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide"> {errorText ? "Error" : "Result"} </h4> <div className={cn( "overflow-x-auto rounded-md text-xs [&_table]:w-full", errorText ? "bg-destructive/10 text-destructive" : "bg-muted/50 text-foreground" )} > {errorText && <div>{errorText}</div>} {Output} </div> </div> );};Usage
import {
Tool,
ToolContent,
ToolHeader,
ToolOutput,
ToolInput,
} from '@/components/ai-elements/tool';<Tool>
<ToolHeader type="tool-call" state={'output-available' as const} />
<ToolContent>
<ToolInput input="Input to tool call" />
<ToolOutput errorText="Error" output="Output from tool call" />
</ToolContent>
</Tool>Usage in AI SDK
Build a simple stateful weather app that renders the last message in a tool using useChat.
Add the following component to your frontend:
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport, type ToolUIPart } from 'ai';
import { Button } from '@/components/ui/button';
import { Response } from '@/components/ai-elements/response';
import {
Tool,
ToolContent,
ToolHeader,
ToolInput,
ToolOutput,
} from '@/components/ai-elements/tool';
type WeatherToolInput = {
location: string;
units: 'celsius' | 'fahrenheit';
};
type WeatherToolOutput = {
location: string;
temperature: string;
conditions: string;
humidity: string;
windSpeed: string;
lastUpdated: string;
};
type WeatherToolUIPart = ToolUIPart<{
fetch_weather_data: {
input: WeatherToolInput;
output: WeatherToolOutput;
};
}>;
const Example = () => {
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: '/api/weather',
}),
});
const handleWeatherClick = () => {
sendMessage({ text: 'Get weather data for San Francisco in fahrenheit' });
};
const latestMessage = messages[messages.length - 1];
const weatherTool = latestMessage?.parts?.find(
(part) => part.type === 'tool-fetch_weather_data',
) as WeatherToolUIPart | undefined;
return (
<div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
<div className="flex flex-col h-full">
<div className="space-y-4">
<Button onClick={handleWeatherClick} disabled={status !== 'ready'}>
Get Weather for San Francisco
</Button>
{weatherTool && (
<Tool defaultOpen={true}>
<ToolHeader type="tool-fetch_weather_data" state={weatherTool.state} />
<ToolContent>
<ToolInput input={weatherTool.input} />
<ToolOutput
output={
<Response>
{formatWeatherResult(weatherTool.output)}
</Response>
}
errorText={weatherTool.errorText}
/>
</ToolContent>
</Tool>
)}
</div>
</div>
</div>
);
};
function formatWeatherResult(result: WeatherToolOutput): string {
return `**Weather for ${result.location}**
**Temperature:** ${result.temperature}
**Conditions:** ${result.conditions}
**Humidity:** ${result.humidity}
**Wind Speed:** ${result.windSpeed}
*Last updated: ${result.lastUpdated}*`;
}
export default Example;Add the following route to your backend:
import { streamText, UIMessage, convertToModelMessages } from 'ai';
import { z } from 'zod';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: 'openai/gpt-4o',
messages: convertToModelMessages(messages),
tools: {
fetch_weather_data: {
description: 'Fetch weather information for a specific location',
parameters: z.object({
location: z
.string()
.describe('The city or location to get weather for'),
units: z
.enum(['celsius', 'fahrenheit'])
.default('celsius')
.describe('Temperature units'),
}),
inputSchema: z.object({
location: z.string(),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
execute: async ({ location, units }) => {
await new Promise((resolve) => setTimeout(resolve, 1500));
const temp =
units === 'celsius'
? Math.floor(Math.random() * 35) + 5
: Math.floor(Math.random() * 63) + 41;
return {
location,
temperature: `${temp}°${units === 'celsius' ? 'C' : 'F'}`,
conditions: 'Sunny',
humidity: `12%`,
windSpeed: `35 ${units === 'celsius' ? 'km/h' : 'mph'}`,
lastUpdated: new Date().toLocaleString(),
};
},
},
},
});
return result.toUIMessageStreamResponse();
}Features
- Collapsible interface for showing/hiding tool details
- Visual status indicators with icons and badges
- Support for multiple tool execution states (pending, running, completed, error)
- Formatted parameter display with JSON syntax highlighting
- Result and error handling with appropriate styling
- Composable structure for flexible layouts
- Accessible keyboard navigation and screen reader support
- Consistent styling that matches your design system
- Auto-opens completed tools by default for better UX
Examples
Input Streaming (Pending)
Shows a tool in its initial state while parameters are being processed.
"use client";import { Tool, ToolContent, ToolHeader, ToolInput } from "@/components/ai-elements/elements/tool";import { nanoid } from "nanoid";const toolCall = { type: "tool-web_search" as const, toolCallId: nanoid(), state: "input-streaming" as const, input: { query: "latest AI market trends 2024", max_results: 10, include_snippets: true, }, output: undefined, errorText: undefined,};const Example = () => ( <div style={{ height: "500px" }}> <Tool> <ToolHeader state={toolCall.state} type={toolCall.type} /> <ToolContent> <ToolInput input={toolCall.input} /> </ToolContent> </Tool> </div>);export default Example;Input Available (Running)
Shows a tool that's actively executing with its parameters.
"use client";import { Tool, ToolContent, ToolHeader, ToolInput } from "@/components/ai-elements/elements/tool";import { nanoid } from "nanoid";const toolCall = { type: "tool-image_generation" as const, toolCallId: nanoid(), state: "input-available" as const, input: { prompt: "A futuristic cityscape at sunset with flying cars", style: "digital_art", resolution: "1024x1024", quality: "high", }, output: undefined, errorText: undefined,};const Example = () => ( <div style={{ height: "500px" }}> <Tool> <ToolHeader state={toolCall.state} type={toolCall.type} /> <ToolContent> <ToolInput input={toolCall.input} /> </ToolContent> </Tool> </div>);export default Example;Output Available (Completed)
Shows a completed tool with successful results. Opens by default to show the results. In this instance, the output is a JSON object, so we can use the CodeBlock component to display it.
"use client";import { CodeBlock } from "@/components/ai-elements/elements/code-block";import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput,} from "@/components/ai-elements/elements/tool";import type { ToolUIPart } from "ai";import { nanoid } from "nanoid";const toolCall: ToolUIPart = { type: "tool-database_query" as const, toolCallId: nanoid(), state: "output-available" as const, input: { query: "SELECT COUNT(*) FROM users WHERE created_at >= ?", params: ["2024-01-01"], database: "analytics", }, output: [ { "User ID": 1, Name: "John Doe", Email: "john@example.com", "Created At": "2024-01-15", }, { "User ID": 2, Name: "Jane Smith", Email: "jane@example.com", "Created At": "2024-01-20", }, { "User ID": 3, Name: "Bob Wilson", Email: "bob@example.com", "Created At": "2024-02-01", }, { "User ID": 4, Name: "Alice Brown", Email: "alice@example.com", "Created At": "2024-02-10", }, { "User ID": 5, Name: "Charlie Davis", Email: "charlie@example.com", "Created At": "2024-02-15", }, ], errorText: undefined,};const Example = () => ( <div style={{ height: "500px" }}> <Tool> <ToolHeader state={toolCall.state} type={toolCall.type} /> <ToolContent> <ToolInput input={toolCall.input} /> {toolCall.state === "output-available" && ( <ToolOutput errorText={toolCall.errorText} output={ <CodeBlock code={JSON.stringify(toolCall.output)} language="json" /> } /> )} </ToolContent> </Tool> </div>);export default Example;Output Error
Shows a tool that encountered an error during execution. Opens by default to display the error.
"use client";import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput,} from "@/components/ai-elements/elements/tool";import type { ToolUIPart } from "ai";const toolCall: ToolUIPart = { type: "tool-api_request" as const, toolCallId: "api_request_1", state: "output-error" as const, input: { url: "https://api.example.com/data", method: "GET", headers: { Authorization: "Bearer token123", "Content-Type": "application/json", }, timeout: 5000, }, output: undefined, errorText: "Connection timeout: The request took longer than 5000ms to complete. Please check your network connection and try again.",};const Example = () => ( <div style={{ height: "500px" }}> <Tool> <ToolHeader state={toolCall.state} type={toolCall.type} /> <ToolContent> <ToolInput input={toolCall.input} /> {toolCall.state === "output-error" && ( <ToolOutput errorText={toolCall.errorText} output={toolCall.output} /> )} </ToolContent> </Tool> </div>);export default Example;Props
<Tool />
Prop
Type
<ToolHeader />
Prop
Type
<ToolContent />
Prop
Type
<ToolInput />
Prop
Type
<ToolOutput />
Prop
Type