ToolCallCardexperimental
A collapsible card for one agent tool call: a status header with the tool name, and JSON input/output (or an error) in the body. It starts expanded when the call fails.
Playground
{
"city": "Berlin",
"unit": "celsius"
} <script lang="ts">
import { ToolCallCard } from '@urbicon-ui/blocks';
const toolCall = {
type: 'tool-call',
id: 'get_weather-1',
name: 'get_weather',
input: { city: 'Berlin', unit: 'celsius' },
state: 'running'
};
</script>
<ToolCallCard
{toolCall}
/>01 Examples
Lifecycle: running to complete
{
"city": "Berlin",
"unit": "celsius"
} <script lang="ts">
import { onDestroy } from 'svelte';
import { Button, ToolCallCard, type ChatToolCallPart } from '@urbicon-ui/blocks';
// The consumer owns the part and mutates its `state` / `output` as the
// real call resolves — here a timer stands in for the transport layer.
let call = $state<ChatToolCallPart>({
type: 'tool-call',
id: 'weather-1',
name: 'get_weather',
state: 'running',
input: { city: 'Berlin', unit: 'celsius' }
});
let timer: ReturnType<typeof setTimeout> | undefined;
function run() {
clearTimeout(timer);
call = { ...call, state: 'running', output: undefined };
timer = setTimeout(() => {
call = {
...call,
state: 'complete',
output: { temperature: 21, condition: 'Partly cloudy', humidity: 0.54 }
};
}, 1600);
}
onDestroy(() => clearTimeout(timer));
</script>
<div class="space-y-3">
<ToolCallCard toolCall={call} />
<Button size="sm" variant="outlined" onclick={run}>Replay call</Button>
</div>
Failure starts expanded
permission denied for relation "invoices" (SQLSTATE 42501)
{
"sql": "SELECT * FROM invoices WHERE due < now()"
} <script lang="ts">
import { ToolCallCard, type ChatToolCallPart } from '@urbicon-ui/blocks';
// A failed call: the card starts expanded so the error is visible without a
// click. `errorMessage` renders above the (still available) input.
const call: ChatToolCallPart = {
type: 'tool-call',
id: 'db-query-7',
name: 'run_query',
state: 'error',
input: { sql: 'SELECT * FROM invoices WHERE due < now()' },
errorMessage: 'permission denied for relation "invoices" (SQLSTATE 42501)'
};
</script>
<ToolCallCard toolCall={call} />
Framed variant for run logs
{
"house": "cala",
"nights": 3
} {
"available": 4
} rate plan expired (RATE_STALE)
{
"room": "sea-view-2"
} {
"room": "sea-view-2",
"refresh": true
} <script lang="ts">
import { ToolCallCard, type ChatToolCallPart } from '@urbicon-ui/blocks';
// A run log: here the calls ARE the content, so each one gets a frame of its
// own instead of the quiet line the chat stream uses.
const trace: ChatToolCallPart[] = [
{
type: 'tool-call',
id: 'trace-1',
name: 'list_rooms',
state: 'complete',
input: { house: 'cala', nights: 3 },
output: { available: 4 }
},
{
type: 'tool-call',
id: 'trace-2',
name: 'price_stay',
state: 'error',
input: { room: 'sea-view-2' },
errorMessage: 'rate plan expired (RATE_STALE)'
},
{
type: 'tool-call',
id: 'trace-3',
name: 'price_stay',
state: 'running',
input: { room: 'sea-view-2', refresh: true }
}
];
</script>
<div class="space-y-2">
{#each trace as call (call.id)}
<ToolCallCard toolCall={call} variant="card" />
{/each}
</div>
Domain-specific body via the children snippet
- 94%
OKLCH in CSS: why we moved
example.com/oklch
- 87%
A perceptual color picker
example.com/picker
- 71%
Gamut mapping explained
example.com/gamut
<script lang="ts">
import { Badge, ToolCallCard, type ChatToolCallPart } from '@urbicon-ui/blocks';
type SearchHit = { title: string; url: string; score: number };
const call: ChatToolCallPart = {
type: 'tool-call',
id: 'search-3',
name: 'web_search',
state: 'complete',
input: { query: 'oklch color space' },
output: [
{ title: 'OKLCH in CSS: why we moved', url: 'example.com/oklch', score: 0.94 },
{ title: 'A perceptual color picker', url: 'example.com/picker', score: 0.87 },
{ title: 'Gamut mapping explained', url: 'example.com/gamut', score: 0.71 }
] satisfies SearchHit[]
};
</script>
<!-- The children snippet replaces the default JSON body with a domain view of
the same part — the header (status + tool name) stays intact. -->
<ToolCallCard toolCall={call}>
{#snippet children(part)}
{@const hits = (part.output ?? []) as SearchHit[]}
<ul class="divide-border-subtle divide-y">
{#each hits as hit (hit.url)}
<li class="flex items-center justify-between gap-3 py-2">
<div class="min-w-0">
<p class="text-text-primary truncate text-sm font-medium">{hit.title}</p>
<p class="text-text-tertiary truncate text-xs">{hit.url}</p>
</div>
<Badge intent="neutral" variant="soft" size="sm">
{(hit.score * 100).toFixed(0)}%
</Badge>
</li>
{/each}
</ul>
{/snippet}
</ToolCallCard>
02 Accessibility
Status is text, not just color
The state label (Pending / Running / Done / Failed) is always in the header, and always exactly
once: the plain header prints it as visible text, the framed one shows it as a decorative Badge (aria-hidden) paired with a single sr-only line. The spinner is decorative in both, so assistive
tech reads the status once and never announces a spinner as content.
Disclosure semantics
The header is a real <button> with aria-expanded and aria-controls pointing at the body region, the same
Collapsible contract as the rest of the library. Tab to reach it, Enter or Space to toggle. Focus rings use focus-visible:.
Untrusted output
Input and output render as plain text inside CodeBlock, not interpreted HTML, because tool results are untrusted data.
03 API Reference
18 propsProp | Type | Default | Description | |
|---|---|---|---|---|
toolCall required | ChatToolCallPart | — | The tool-call part to render. Required. | |
callState variant | completeerrorpendingrunning | pending | Controls the callState behavior and appearance of the ToolCallCard component. Available options: complete, error, pending, running. | |
children | Snippet<[ChatToolCallPart]> | — | Replace the default JSON input/output body with a custom rendering of the
tool-call part. Receives the same toolCall. When provided, the built-in
error line + input/output sections are not rendered. | |
class | string | — | Extra classes merged onto the root element. | |
completeLabel | string | 'Done' | Header status label for the complete state. | |
defaultOpen | boolean | — | Initial expanded state for uncontrolled usage. Defaults to true when the
call is already in the error state, false otherwise. | |
errorLabel | string | 'Failed' | Header status label for the error state. | |
inputLabel | string | 'Input' | Caption in the input payload's header. | |
onOpenChange | (open: boolean) => void | — | Fired once per toggle, after the new open state is applied. | |
open | boolean | — | Whether the card is expanded. Supports bind:open. Left uncontrolled, the
card starts collapsed for pending / running / complete and expanded
for error. | |
outputLabel | string | 'Output' | Caption in the output payload's header. | |
pendingLabel | string | 'Pending' | Header status label for the pending state. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ ToolCallCard: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette. | |
runningLabel | string | 'Running' | Header status label for the running state. | |
slotClasses | Partial<Record<ToolCallCardSlots, string>> | — | Per-slot class overrides. Slots: trigger (header button), triggerLeft,
triggerRight, spinner, toolName, statusText (the plain header's
status; card uses a Badge instead), chevron, body, section,
errorMessage. The payloads render as variant="plain" CodeBlocks — style
those through <BlocksProvider presets={{ CodeBlock: {...} }}>. | |
unstyled | boolean | — | Strip the component's default tv() classes, the underlying Collapsible's and the status Badge's. | |
variant | plaincard | 'plain' | How prominent the header is. plain is one muted line in the message
flow, with no outline, surface, or badge, as wide as its own text. card
is a framed header (outline, radius, shadow, status badge, the container's
full width) for surfaces where the tool call is the subject. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'class' | 'children') |
04 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
ToolCallCardProps | interface | props | 0 | — | |
ChatToolCallPart | type | helper | 1 | The tool-call member of ChatMessagePart — the prop shape of ToolCallCard. | |
ToolCallCardSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. | |
ChatMessagePart | type | helper | 0 | One renderable segment of a message. Mirrors the shape of modern model/tool transcripts: interleaved text, reasoning, tool calls, sources and attachments — rendered in order by the ChatMessage component. | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: | |
CitationSource | interface | helper | 0 | A cited source surfaced behind a [id] citation marker. id keys the
source to its marker; title is always shown, url / snippet are optional
and only render when present (and, for url, when the URL policy allows it). |
05 Installation
Import
import { ToolCallCard } from '@urbicon-ui/blocks';
import type { ChatToolCallPart } from '@urbicon-ui/blocks';