# 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`](https://pixilui.com/components/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`](https://pixilui.com/components/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`](https://pixilui.com/components/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`](https://pixilui.com/components/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`](https://pixilui.com/components/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`](https://pixilui.com/components/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](https://pixilui.com/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.
```
