Migrate from AI Elements

Move the agent parts of a chat or run screen from Vercel AI Elements to PixilUI. Both install with the shadcn CLI into their own folders, so you can switch one part at a time and keep the rest of AI Elements.

Before you start#

  • The AI Elements site says the project is moving to shadcn/ui and will be deprecated at the end of November 2026. Its components keep working where they are installed; this guide is for the parts you want to replace.
  • PixilUI items take plain props, not AI SDK message parts. You map each part to props in a line or two (examples below), which also means the items work with any SDK or your own stream.
  • AI Elements lives in components/ai-elements/, PixilUI in components/pixilui/. Neither overwrites the other.
  • PixilUI reads its own tokens from foundation.css. Move a whole message or panel at a time so one surface never mixes the two looks.

What maps to what#

AI Elements PixilUI What changes
Tool and its parts tool-call-row One component for Tool, ToolHeader, ToolContent, ToolInput and ToolOutput. Map the part's state to status (see below), pass input, output and errorText as input, output and error. Write summary as what the call did in plain words. Wrap the calls of one message in ToolCallRowGroup.
Confirmation and its parts approval-card Show what the agent will do as items rows with a risk. onApprove and onReject send the approval response; return the promise and the card shows pending, then folds into a receipt.
Reasoning thinking-state isStreaming becomes done={!isStreaming}; the timer runs from startedAt. Pass short steps instead of the raw reasoning text, or keep Reasoning if you show the full text.
Loader, Shimmer thinking-state While waiting for the first token: a line that says what the agent is doing, with a live timer, instead of a spinner.
Response, MessageResponse streaming-text text and streaming; only new words animate and half-arrived words wait. It renders plain text with inline code, not full markdown: keep Response for answers with lists, tables or code blocks.
Task, ChainOfThought, Plan run-timeline Steps with id, title, detail and status (pending, active, done, error, waiting). A waiting step holds an approval card.
Conversation and the rest None yet Keep AI Elements or shadcn/ui for Conversation, Message, PromptInput, CodeBlock, Sources, InlineCitation, WebPreview and Canvas. The roadmap lists what is planned.

Steps#

1. Add the registry and the foundation

bash
npx pixilui@latest init

This adds @pixilui next to @ai-elements in components.json, installs the foundation and writes the agent rules.

2. Add the items

bash
npx pixilui@latest add tool-call-row approval-card thinking-state

3. Map the message parts to props

Where you render a tool part today:

tsx
// Before
<Tool>
  <ToolHeader type={part.type} state={part.state} />
  <ToolContent>
    <ToolInput input={part.input} />
    <ToolOutput output={part.output} errorText={part.errorText} />
  </ToolContent>
</Tool>

// After
import { ToolCallRow, type ToolCallRowValue } from "@/components/pixilui/tool-call-row";

const status = (state: string) => (state === "output-error" ? "error" : state === "output-available" ? "done" : "running");

<ToolCallRow
  name={part.type.replace(/^tool-/, "")}
  summary={describe(part)}
  status={status(part.state)}
  input={part.input as ToolCallRowValue}
  output={part.output as ToolCallRowValue}
  error={part.errorText}
/>

describe is yours: one short past-tense line per tool ("Found 3 overdue invoices for Northwind"). It is the line people read, so it is worth writing.

For reasoning:

tsx
// Before
<Reasoning isStreaming={status === "streaming"}>
  <ReasoningTrigger />
  <ReasoningContent>{part.text}</ReasoningContent>
</Reasoning>

// After
<ThinkingState label="Reading the incident timeline" startedAt={startedAt} done={status !== "streaming"} />

4. Replace the approval step

Where an AI Elements Confirmation asks before a tool runs, render an approval card and send the same response from its callbacks:

tsx
<ApprovalCard
  title="Send 7 renewal reminders"
  risk="medium"
  items={[
    { label: "Audience", value: "7 customers with a card expiring in November" },
    { label: "Cost", value: "$0.42", numeric: true },
  ]}
  onApprove={() => respond({ id: part.approval.id, approved: true })}
  onReject={(note) => respond({ id: part.approval.id, approved: false, reason: note })}
/>

respond is the approval call your SDK gives you; with the AI SDK it is the approval response helper from useChat. Return its promise so the card shows its own pending state.

5. Keep the rest

Leave Conversation, Message, PromptInput and the other parts with no match where they are. Remove an AI Elements file only when nothing imports it.

With an agent#

Paste this into Claude Code, Cursor or Codex:

text
Read https://pixilui.com/docs/migrate-from-ai-elements/markdown. In the chat page, replace the AI Elements Tool, Confirmation and Reasoning parts with PixilUI tool-call-row, approval-card and thinking-state using that mapping. Keep everything else. List what you changed.