Trace is a list of lines. Each line has an icon, a title, and an optional description and status. A line can expand to show more detail, link to a page, or open a dialog or sheet.
Every line has the same shape, so reasoning, tool calls, sources and plans built from Trace look and behave the same.
Use Trace for the steps an agent takes, like calling a tool, running a subagent or editing a file. Use Message for its reply, and Marker for notes and dividers between turns.
Built with Trace
Every pattern below is a composition of Trace parts. See AI Elements for examples and code. AI Elements is shadcn/ui's React library, so its code is React.
Installation
Install the dependencies
Add the Collapsible component
Copy the component
Copy the following files into src/lib/components/ui/trace.
These files use the Nova style and Lucide icons. npx @sveltery/ui@latest add writes the style and icon library from your components.json.
It imports the collapsible component, so copy its folder too.
<script lang="ts">
import type { TraceProps } from './types.js';
import { cn } from 'cn';
let { children, class: classProp, ...props }: TraceProps = $props();
</script>
<div
data-slot="trace"
{...props}
class={cn(
'gap-2 *:has-[>[data-slot=trace-line]]:rounded-md *:has-[>[data-slot=trace-line]]:focus-visible:ring-3 *:has-[>[data-slot=trace-line]]:focus-visible:ring-ring/50 flex w-full min-w-0 flex-col *:has-[>[data-slot=trace-line]]:flex *:has-[>[data-slot=trace-line]]:w-fit *:has-[>[data-slot=trace-line]]:max-w-full *:has-[>[data-slot=trace-line]]:min-w-0 *:has-[>[data-slot=trace-line]]:text-left *:has-[>[data-slot=trace-line]]:outline-none',
classProp,
)}
>
{@render children?.()}
</div> <script lang="ts">
import type { TraceCollapsibleProps } from './types.js';
import { cn } from 'cn';
import { Collapsible } from '../collapsible/index.js';
let { open = $bindable(), class: classProp, ...props }: TraceCollapsibleProps = $props();
</script>
<Collapsible data-slot="trace-collapsible" bind:open {...props} class={cn('flex w-full min-w-0 flex-col', classProp)} /> <script lang="ts">
import type { TraceCollapsibleContentProps } from './types.js';
import { cn } from 'cn';
import { CollapsibleContent } from '../collapsible/index.js';
let { children, class: classProp, ...props }: TraceCollapsibleContentProps = $props();
</script>
<CollapsibleContent
data-slot="trace-collapsible-content"
class="duration-200 h-(--collapsible-panel-height) min-w-0 overflow-hidden transition-[height] data-ending-style:h-0 data-starting-style:h-0"
{...props}
>
<div data-slot="trace-collapsible-content-inner" class={cn('mt-2 mb-1 ml-[calc(--spacing(2)-1px)] border-l pr-1 pl-4 leading-relaxed min-w-0 wrap-break-word text-muted-foreground', classProp)}>
{@render children?.()}
</div>
</CollapsibleContent> <script lang="ts">
import type { TraceCollapsibleTriggerProps } from './types.js';
import { cn } from 'cn';
import { CollapsibleTrigger } from '../collapsible/index.js';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import TraceIcon from './TraceIcon.svelte';
let { children, class: classProp, ...props }: TraceCollapsibleTriggerProps = $props();
</script>
<CollapsibleTrigger
data-slot="trace-collapsible-trigger"
{...props}
class={cn(
'gap-1 rounded-md focus-visible:ring-3 focus-visible:ring-ring/50 group/trace-collapsible-trigger flex w-fit max-w-full min-w-0 cursor-pointer items-center text-left text-muted-foreground transition-colors outline-none hover:text-foreground has-[[data-status=error]]:text-destructive',
classProp,
)}
>
{@render children?.()}
<TraceIcon class="transition-transform group-aria-expanded/trace-collapsible-trigger:rotate-90">
<ChevronRightIcon />
</TraceIcon>
</CollapsibleTrigger> <script lang="ts">
import type { TraceDescriptionProps } from './types.js';
import { cn } from 'cn';
let { children, class: classProp, ...props }: TraceDescriptionProps = $props();
</script>
<span data-slot="trace-description" {...props} class={cn('shrink-0 text-muted-foreground/70 tabular-nums', classProp)}>
{@render children?.()}
</span> <script lang="ts">
import type { TraceIconProps } from './types.js';
import { cn } from 'cn';
let { children, class: classProp, ...props }: TraceIconProps = $props();
</script>
<span data-slot="trace-icon" aria-hidden="true" {...props} class={cn("size-4 [&_svg:not([class*='size-'])]:size-4 [[data-slot=trace-title]~&]:-ml-1 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none", classProp)}>
{@render children?.()}
</span> <script lang="ts">
import type { TraceLineProps } from './types.js';
import { cn } from 'cn';
let { children, class: classProp, status, ...props }: TraceLineProps = $props();
</script>
<span
data-slot="trace-line"
data-status={status}
{...props}
class={cn(
'gap-2 text-sm flex w-fit max-w-full min-w-0 items-center text-muted-foreground transition-colors in-[a,button]:cursor-pointer in-[a:hover,button:hover]:text-foreground data-[status=error]:text-destructive data-[status=error]:in-[a:hover,button:hover]:text-destructive data-[status=pending]:opacity-60 data-[status=running]:*:data-[slot=trace-title]:shimmer',
classProp,
)}
>
{@render children?.()}
</span> <script lang="ts">
import type { TraceTitleProps } from './types.js';
import { cn } from 'cn';
let { children, class: classProp, ...props }: TraceTitleProps = $props();
</script>
<span data-slot="trace-title" {...props} class={cn('min-w-0 truncate motion-reduce:shimmer-none', classProp)}>
{@render children?.()}
</span> import type { ComponentProps } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../collapsible/index.js';
type NativeSpanProps = HTMLAttributes<HTMLSpanElement>;
export type TraceProps = HTMLAttributes<HTMLDivElement>;
export type TraceLineProps = NativeSpanProps & { status?: 'running' | 'error' | 'pending' };
export type TraceIconProps = NativeSpanProps;
export type TraceTitleProps = NativeSpanProps;
export type TraceDescriptionProps = NativeSpanProps;
export type TraceCollapsibleProps = ComponentProps<typeof Collapsible>;
export type TraceCollapsibleTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
export type TraceCollapsibleContentProps = ComponentProps<typeof CollapsibleContent>; export { default as Trace } from './Trace.svelte';
export { default as TraceLine } from './TraceLine.svelte';
export { default as TraceIcon } from './TraceIcon.svelte';
export { default as TraceTitle } from './TraceTitle.svelte';
export { default as TraceDescription } from './TraceDescription.svelte';
export { default as TraceCollapsible } from './TraceCollapsible.svelte';
export { default as TraceCollapsibleTrigger } from './TraceCollapsibleTrigger.svelte';
export { default as TraceCollapsibleContent } from './TraceCollapsibleContent.svelte';
export type {
TraceProps,
TraceLineProps,
TraceIconProps,
TraceTitleProps,
TraceDescriptionProps,
TraceCollapsibleProps,
TraceCollapsibleTriggerProps,
TraceCollapsibleContentProps,
} from './types.js'; Update the import paths
The component folders import each other with relative paths, so keep them side by side. If you place them elsewhere, change the imports to match your project setup.
Usage
Use a TraceLine for a single step:
<script lang="ts">
import SearchIcon from '@lucide/svelte/icons/search';
import { Trace, TraceIcon, TraceLine, TraceTitle } from '#lib/components/ui/trace';
</script> <Trace>
<TraceLine>
<TraceIcon>
<SearchIcon />
</TraceIcon>
<TraceTitle>Searched for minimumReleaseAge</TraceTitle>
</TraceLine>
</Trace> Wrap the line in a TraceCollapsible to show more detail when it expands:
<script lang="ts">
import TerminalIcon from '@lucide/svelte/icons/terminal';
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from '#lib/components/ui/trace';
</script> <Trace>
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<TerminalIcon />
</TraceIcon>
<TraceTitle>Ran node --version</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>v24.18.0</TraceCollapsibleContent>
</TraceCollapsible>
</Trace> Composition
Use the following composition to build a trace line:
Trace
└── TraceLine
├── TraceIcon
├── TraceTitle
├── TraceDescription
└── TraceIcon Wrap the line in a TraceCollapsible to show more detail when it expands:
Trace
└── TraceCollapsible
├── TraceCollapsibleTrigger
│ └── TraceLine
└── TraceCollapsibleContent TraceLine is always the line. To make a line do something, wrap it in that thing's trigger.
Features
- One row for reasoning, tool calls, sources, plans and tasks
- Rows that expand, link out, or open a dialog, sheet, drawer or popover
running,errorandpendingstates, with a shimmer while running- Nested rows, so a summary expands into steps and a step into its output
- Works with reasoning, tool and source parts from the AI SDK
- Keyboard and screen reader support from the trigger that wraps each row
- Customizable styling through the
classprop on every part
Trace Line
Use a TraceLine on its own for a step with no detail to show. It reads as plain text.
Status
Set status on TraceLine when a step is running, has failed or hasn't started yet.
Description
Put secondary text such as a diff stat, a duration or a count in a TraceDescription after the TraceTitle. It does not truncate.
Collapsible
Wrap the line in a TraceCollapsible to show more detail when it expands. Put the TraceLine inside TraceCollapsibleTrigger and the detail in TraceCollapsibleContent.
Open State
Rows start closed. Set open on TraceCollapsible to start open, or bind:open to read and set a row's state.
To open a row while it streams and let the reader's click win, bind open to a getter and a setter.
<script lang="ts">
let userOpen = $state<boolean>();
</script>
<TraceCollapsible bind:open={() => userOpen ?? running, (next) => (userOpen = next)}>
<TraceCollapsibleTrigger>
<TraceLine status={running ? 'running' : undefined}>…</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>{text}</TraceCollapsibleContent>
</TraceCollapsible> For a list of rows, keep each row's click in a SvelteMap keyed by message and position, as the Reasoning example does.
Link
Wrap the TraceLine in an anchor. For a new tab, use an arrow-up-right icon and sr-only "(opens in a new tab)".
<a href="https://docs.astro.build" target="_blank" rel="noreferrer">
<TraceLine>
<TraceTitle>
Install Astro
<span class="sr-only"> (opens in a new tab)</span>
</TraceTitle>
<TraceIcon>
<ArrowUpRightIcon />
</TraceIcon>
</TraceLine>
</a> Dialog
Wrap the TraceLine in the dialog's trigger. The dialog owns its open state, Escape and focus return.
<Dialog>
<DialogTrigger>
<TraceLine>
<TraceTitle>Searched the web for cache keys</TraceTitle>
<TraceIcon>
<ChevronRightIcon />
</TraceIcon>
</TraceLine>
</DialogTrigger>
<DialogContent>…</DialogContent>
</Dialog> Sheet
The same shape as the dialog. Drawer and Popover work the same way.
Group
Put rows that sit next to each other in a Trace. It also styles any trigger or link that wraps a TraceLine.
Sources
A collapsible row whose detail is a Trace of link rows.
See Sources for more examples.
Chain of Thought
A chain of thought is a collapsible row with step rows inside. Set the parent's status and title from its children.
See Chain of Thought and Task for more examples.
Commit
A commit is a collapsible row with a line for each changed file. Say what changed in the title, Added, Edited or Deleted, and put the line counts in the description. Each file opens its diff in a Sheet.
Whole Turn
Three levels: the turn, a summary, and the rows. Text between steps sits between two Trace lists.
Reasoning
Render a reasoning part from the AI SDK. The row is a TraceLine until the first text arrives. See Open State.
See Reasoning for more examples.
Tool
Render tool parts from the AI SDK. Map the tool part as in Tool.
See Tool for more examples.
Accessibility
Trace adds no roles of its own. Each line gets its semantics from the element that wraps it.
- A
TraceLineon its own is aspan. It is read as text and is not a tab stop. - Wrap it in a
TraceCollapsibleTriggerand it becomes a button witharia-expanded. Wrap it in a link, or in the trigger for a dialog or sheet, and it takes the keyboard and focus behavior of that element. TraceIconis hidden from assistive tech, soTraceTitleis the accessible name. Write it as a sentence that says what happened, like "Edited trace.svelte", not just "trace.svelte".- When a link opens a new tab, add "(opens in a new tab)" to the title with
sr-only. - Status is shown with color and motion, so put it in the title too. Write "Running pnpm build" while it runs and "Failed to run pnpm build" when it errors.
- The shimmer on a running title stops when the reader prefers reduced motion. The title still says what is happening.
- Inside a Message Scroller, each line is announced once when it is added.
TraceCollapsibleContentmutes its text. If you put a block with a muted background inside it, give the blocktext-foregroundso it keeps its contrast.CodeBlocksets its own text color.
API reference
Trace
A list of rows. It also styles any trigger or link that wraps a TraceLine.
TraceLine
One line: an icon and a title. It is a span, so it is not a button or a tab stop. Wrap it in a trigger or a link to make it do something.
TraceIcon
A decorative icon slot, hidden from assistive tech. Before the title it names the action; after it, it is the indicator.
TraceTitle
One sentence on one line. It truncates and shimmers while the line is running.
TraceDescription
Secondary text after the title, such as a count or a duration. It does not truncate.
TraceCollapsible
A row that expands. It accepts the props of Collapsible.
TraceCollapsibleTrigger
The button that opens and closes the row. Put a TraceLine inside it. It adds a chevron after the line.
TraceCollapsibleContent
The detail of a row that expands.