DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChatBubbleStreamingResponseCitationsToolCallPlanListParameterDiffModelSelectorElicitationArtifactPanelPromptInputKeyboardToolbar
AI agentsContributing
  1. Components
  2. ParameterDiff

ParameterDiff

What an agent's action will change, before anyone approves it — old and new values side by side or merged inline, marked by glyph and word as well as colour, and read out as one sentence per row.

$ pnpm add @tessinaui/ui

When to use

  • Before an approval. An agent is about to write parameters or a record, and the reader should see exactly what changes before they say yes.
  • A "review your changes" step. A settings form or an import can end on the list of values that will be written, so nothing is a surprise.
  • Values the reader can compare in a glance. Short strings, numbers, booleans and small objects fit a row; a long value is cut with a Show more toggle rather than pushing the rest away.
  • Inputs that arrive over time. Set streaming and the reader watches changes appear without the rows moving.
  • A change that must not rely on colour. Every row carries a glyph and a spoken word as well as its tint, so added, removed and modified read the same in greyscale and to a screen reader.

When not to use

  • The call itself. Use ToolCall, which carries the tool's name, status, arguments and the Approve / Deny decision. Put a ParameterDiff inside it when the arguments are a change to existing values.
  • How risky the change is. ToolCall's risk prop carries the tier and its effects slot takes a ParameterDiff as what approving will do; this component shows what changes, not how dangerous it is.
  • Source code or long documents. A line diff with context and hunks is a code-review component. This one compares values, word by word.
  • Collecting new values. Use Elicitation to ask for input; a diff only shows what is already decided.
  • A change nobody needs to check. If the reader would approve it without looking, a confirmation sentence is enough and a table of rows is noise.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { ParameterDiff } from "@tessinaui/ui/parameter-diff";

<ParameterDiff
  changes={[
    { name: "limit", before: 10, after: 25 },
    { name: "sort", before: "asc", after: "desc" },
    { name: "owner", after: "platform" },
  ]}
/>

Each change is a name with an optional before and after, and its kind follows from which are there: no before is added, no after is removed, and both are modified — or unchanged when they render to the same text. Values can be anything: a string renders as itself, a number or boolean as its text, and an object or array as JSON.

The diff is a snapshot. Pass the changes as they stand on every render; it keeps no state of its own except what the reader has expanded.

Examples

Default

Modes

Side by side (the default) puts the old value and the new one in two columns, and stacks them below 768px. Inline merges them into one value, striking the removed words and underlining the added ones.

Prose diff

TextDiff diffs a passage word by word as running text — an edit an agent proposes to a paragraph, two versions of a prompt. Removed words are a struck del, added words an underlined ins (unless underline is off), each opened by a hidden "Removed:" or "Added:". A change never spans a line break unless glyphs is set, and a passage with no words in common reads as one removal and one addition.

The root keeps the passage's own line breaks and collapses a space at a wrap, so a marked run never carries a stray space at the end of a line. Added words are underlined by default; set underline={false} only on a surface where another cue already marks additions, such as a tinted row. Set glyphs for a passage that changes by the line: each mark opens with + or − and a changed run stays whole across its line breaks.

Pass tokenize so a unit of your own, such as {{variable}}, is compared whole, and renderText to draw each run's text — here, variables as chips.

Streaming

Long values and many rows

A value past 160 characters is cut with a Show more toggle, and rows past maxVisible wait behind a Show N more changes toggle.

Unchanged rows

On color

RTL

Loading

Streaming

The diff follows the streaming-stable contract every AI-kit component shares (CONVENTIONS §7c):

  • Any prefix renders. No props, an empty list, a name cut mid-word — each renders, none throws. A change with neither value yet waits until one arrives, and a change that is not an object is skipped.
  • Arrived rows never move. Rows are keyed by position, because a stream only appends and a name can still be arriving. A modified row reads as removed until its new value arrives, then settles in place.
  • Rows never disappear mid-stream. While streaming is set, unchanged rows are shown: a value can equal the old one for a moment ("1" on its way to "12"), and hiding the row would move every row below it.
  • Streaming is never announced. While streaming is set the diff is aria-busy with aria-live="off". The card around it announces the transitions; the diff has no live region of its own.

API Reference

ParameterDiff

PropTypeDefaultDescription
changesParameterChange[][]{ name, before?, after? } per row, in order
mode"side-by-side" | "inline""side-by-side"Two columns (stacked below 768px), or one merged value per row
showUnchangedbooleanfalseAlso render rows whose value did not change. While streaming, they always render
maxVisiblenumber10Rows shown before a "Show N more changes" toggle
streamingbooleanfalseMarks the diff aria-busy and silent while changes arrive
labelstring"Changes"The list's accessible name
tone"default" | "on-color"inheritedInherited from a wrapping <Surface>, else "default"
dir"ltr" | "rtl"—Text direction; "rtl" also picks the Arabic strings
stringsPartial<ParameterDiffStrings>—Per-entry string overrides

Everything else lands on the root.

Also exported: PARAMETER_DIFF_STRINGS (the English and Arabic tables), PARAMETER_DIFF_VALUE_PREVIEW_CHARS (160), PARAMETER_DIFF_MAX_ROWS (200), and diffWords(before, after, { tokenize }), the word diff the inline mode and TextDiff use.

TextDiff

PropTypeDefaultDescription
beforestring—The passage as it is
afterstring—The passage after the change
tokenize(text: string) => string[]word segmentsTokens to compare; they must join back to the text exactly
renderText(text, kind) => ReactNodethe textRenders each run's text
underlinebooleantrueUnderline added words. With the strike on removed words it keeps a change from resting on colour alone; turn it off only where another cue already marks additions
glyphsbooleanfalseOpen each mark with a + or − and keep a changed run whole, line breaks included, so an added line reads as one block. For passages that change by the line, like a prompt
tone"default" | "on-color"inheritedInherited from a wrapping <Surface>
dir"ltr" | "rtl"—Text direction; "rtl" also picks the Arabic strings
stringsPartial<TextDiffStrings>—The spoken removed / added words; an empty string skips one

Everything else lands on the root span. Past the word-diff caps the whole old passage reads removed and the whole new one added.

ParameterDiffSkeleton

PropTypeDefaultDescription
rowsnumber3How many rows the placeholder stands in for
mode"side-by-side" | "inline""side-by-side"Match the diff it replaces
labelstring—What AT announces while the placeholder is up. Silent without it
dir"ltr" | "rtl"—Text direction

DOM contract

AttributeWhereValues
data-slotevery partparameter-diff, parameter-diff-list, parameter-diff-row, parameter-diff-marker, parameter-diff-name, parameter-diff-value, parameter-diff-before, parameter-diff-after, parameter-diff-empty-side, parameter-diff-run, parameter-diff-reveal, parameter-diff-show-all, parameter-diff-omitted, parameter-diff-no-changes, parameter-diff-skeleton, text-diff, text-diff-mark
data-kindeach row and each runrow: added · removed · modified · unchanged; run: same · removed · added
data-modethe rootside-by-side · inline
data-tonethe rootdefault · on-color

Accessibility

There is no APG pattern for a diff. The diff is a named list, one row per change, and the contract is:

  • Each row reads as one sentence. The DOM order is the reading order — name, kind, old value, new value — so a screen reader walking the list hears "limit, Modified, was 10, now 25", "url, Added, now https://x.dev" and "timeout, Removed, was 30". "Was" and "now" are visually hidden text, so the sentence reads the same in either mode.
  • Stacking never reorders. Below 768px the two columns stack; that moves where the values are drawn, never the order they are read in.
  • Inline runs are read as a sentence too. The merged word runs would read "The quickslow fox" in DOM order, so they are hidden from assistive technology and the row speaks "was The quick fox, now The slow fox" instead.
  • A change is never carried by colour alone. Each kind has a glyph (plus, minus, pencil) and a spoken word; removed text is struck through, and added words inside a modified value are underlined.
  • Every changed row is reachable by keyboard. Nothing scrolls sideways, long values wrap, and everything hidden sits behind a real button — the "Show more" and "Show N more changes" toggles, each with aria-expanded. Rows are content, not tab stops: Tab moves from one toggle to the next.
  • Focus never drops. The row toggle stays in place when it expands and flips to "Show fewer changes", so focus stays on it.
  • TextDiff marks are real edits. A passage has no parallel sentence to read, so its removed and added words are del and ins elements, each opened by a hidden "Removed:" or "Added:" for assistive technology that does not announce them. Override either word with strings; an empty string skips it.
  • Reduced motion: nothing animates.

AT matrix: not yet screen-reader tested, hence beta.

Notes

  • Unchanged rows are hidden by default. A review screen is about what changes; pass showUnchanged to show the rest for context.
  • Words, not lines. The inline mode and TextDiff compare word by word, so an edit in the middle of a long prompt marks only the words that changed. A change never spans a line break. Past 2,000 characters or 400 words a side, and while a value is cut, the whole value is marked as replaced instead.
  • There is a hard cap. No more than 200 rows ever render, even after "Show all"; past it a line says how many were left out.
PlanListModelSelector

On this page

When to useWhen not to useInstallationUsageExamplesDefaultModesProse diffStreamingLong values and many rowsUnchanged rowsOn colorRTLLoadingStreamingAPI ReferenceParameterDiffTextDiffParameterDiffSkeletonDOM contractAccessibilityNotes