Installation
npx shadcn@latest add @pixeldosa/ai-context-surfaceRequires `cn` at `@/lib/utils`. `generatedAt` is formatted internally (relative within 24 hours via Intl.RelativeTimeFormat, an absolute locale date otherwise) rather than left to the consumer, since every consumer would otherwise reimplement the same relative-or-absolute decision. Renders `null` entirely when none of `explanation`/`sources`/`model`/`generatedAt` are supplied — a trigger with nothing behind it is worse than no component.
Usage
import { AIContextSurface } from "@/components/ui/ai-context-surface";
<AIContextSurface
explanation="Matched based on similar historical records with the same domain."
sources={[{ label: "acme-robotics.com", url: "https://acme-robotics.com" }]}
model="claude-opus"
generatedAt={new Date()}
/>
Every prop is optional. If none are supplied, the component renders nothing — a trigger with an empty panel behind it would be worse than no component at all.
Sources with a snippet, no link
<AIContextSurface
sources={[
{ label: "Similar record: Acme Manufacturing", snippet: "Same domain root, added 3 weeks ago." },
]}
/>
A source only needs a label; url and snippet are both optional and independent —
a source can have a link, a snippet, both, or neither.
Features
- Quiet until asked for. Collapsed by default, so provenance never competes with the value it explains — this is the reference implementation of DESIGN.md §4's "quiet until asked for" instinct.
- Renders nothing when it has nothing. Every prop is optional; with none supplied
the component returns
nullrather than offering a trigger with an empty panel. - Evidence over decoration — sources carry an optional link and an optional snippet, independently, so you can show the actual retrieved text rather than just asserting a source exists.
- A real disclosure, with
aria-expanded/aria-controlswired correctly, not a custom widget that merely looks like one. - No chat thread required — it attaches to a single value anywhere in ordinary product UI.
Variants
Explanation only — the minimum useful surface:
Matched on the same domain root as three existing records, added within the last month.
With sources and model metadata
Matched based on similar historical records with the same domain.
- 1.acme-robotics.com
- 2.Similar record: Acme ManufacturingSame domain root, added 3 weeks ago.
claude-opus
A custom trigger label
triggerLabel is both the visible and the accessible label, so it should read as a
question the user actually has.
- 1.Q3 pipeline exportRow 148, imported 2 weeks ago.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
explanation | string | — | Plain-language reasoning, shown first. |
sources | { label: string; url?: string; snippet?: string }[] | — | Rendered as a numbered list. |
model | string | — | Shown in the metadata footer line. |
generatedAt | string | Date | — | Formatted internally (relative within 24h, absolute otherwise). |
triggerLabel | string | "Why this?" | Visible and accessible label for the collapsed trigger. |
Accessibility
- A real disclosure:
aria-expandedandaria-controlswired correctly, not a custom widget that only looks like one. - Panel content is normal reading-order text, not a floating overlay, so it reflows naturally at any width.
- Under
prefers-reduced-motion, the expand/collapse becomes instant;aria-expandedcommunicates the state to assistive technology regardless of any icon animation.
When not to use
A full chat/conversation transcript is out of this system's scope entirely. A single
short provenance caption for one value already has a home — Smart Field's own
inline provenance prop — use that instead of this heavier disclosure for a one-line
via {source} caption.
why it's built this way
Engineering Notes
Deliberately a disclosure (collapsed by default), not an always-visible panel: provenance detail is important to have available but wrong to force onto every view, since it would compete with the actual content for attention the moment it's always open — the same restraint Smart Field's inline `via {provenance}` caption already established, applied here at a larger, richer scale. Folds three previously-separate roadmap ideas (Citation Card, Source Viewer, Thinking Indicator) into one component because all three share the same real constraint this system cares about: working with zero chat thread present, unlike Vercel AI Elements' `Reasoning`/citation components, which are chat-message-scoped and excellent at that job specifically — competing with them there would be a bad trade, per the roadmap's own Reconciliation table. Timestamp formatting uses native `Intl.RelativeTimeFormat` rather than a date-formatting dependency, keeping this primitive dependency-free like every other component in the registry.
how it moves, and when it doesn't
Motion Notes
Expand/collapse reuses the same CSS grid-template-rows (0fr to 1fr) technique Smart Field's proposal panel and Diff Accept's conflict banner already use — a proven, token-free way to animate a height that depends on variable content, on --pd-duration-fast/--pd-ease-standard. The trigger's chevron rotates 90 degrees on open using --pd-duration-instant, matching Button's press-feedback timing, since it's an immediate state confirmation rather than content arriving. Under prefers-reduced-motion, both the panel's height transition and the chevron rotation drop via motion-reduce:transition-none, leaving the open/closed state instant but fully legible — aria-expanded already communicates the state to assistive technology regardless of the icon's rotation.