# Migrate from shadcn/ui

Move a screen from shadcn/ui components to PixilUI one item at a time. Both install with the same CLI, and PixilUI lands in its own folder, so the two run side by side while you switch.

## Before you start

- Nothing is replaced on install. shadcn/ui lives in `components/ui/`, PixilUI in `components/pixilui/`, and neither overwrites the other.
- PixilUI reads its own tokens from `foundation.css`, not your shadcn/ui theme variables. A PixilUI button next to a shadcn/ui card will use the PixilUI colours, radii and shadows. The [theming page](https://pixilui.com/docs/theming) says how to point the PixilUI roles at your brand.
- PixilUI overlays use Base UI, not Radix. They do not share context with Radix components, so move a dialog and the buttons inside it together.
- Migrate a whole screen at a time. Mixing the two sets of tokens on one surface is the one thing that looks wrong.

## What maps to what

| shadcn/ui | PixilUI | What changes |
| --- | --- | --- |
| `Button` | [`button`](https://pixilui.com/components/button) | `variant`: `default` is `primary`, `destructive` is `danger`, `secondary` and `ghost` keep their names. `outline` and `link` have no match: keep shadcn/ui for those. `size`: `default` is `md`. `loading` shows a spinner in place of the icon and keeps focus; icons go in `icon` and `iconEnd`, not in the children. |
| `Dialog` | [`dialog`](https://pixilui.com/components/dialog) | The trigger is a prop (`trigger={<Button>…</Button>}`), the content is the children. `DialogTitle` and `DialogDescription` keep their names. `DialogFooter` with two buttons becomes `DialogActions confirm="Delete agent" onConfirm={…}`, which shows pending and error itself when `onConfirm` returns a promise. |
| `AlertDialog` | [`dialog`](https://pixilui.com/components/dialog) with `variant="danger"` | One component for both. If the thing being confirmed is an action an agent wants to take, use [`approval-card`](https://pixilui.com/components/approval-card) instead. |
| `Sheet`, `Drawer` | [`drawer`](https://pixilui.com/components/drawer) | `side` is `right`, `left` or `bottom` (no `top`). `DrawerTitle`, `DrawerDescription`, `DrawerActions` and `DrawerClose` match the dialog parts. |
| `Popover` | [`popover`](https://pixilui.com/components/popover) | The trigger is a prop; `side`, `align` and `offset` work as in shadcn/ui. `PopoverTitle`, `PopoverDescription` and `PopoverClose` are optional. |
| `Tooltip` | [`tooltip`](https://pixilui.com/components/tooltip) | The text is `label`, the control is the child, and `shortcut` shows the key. `TooltipProvider` is optional: a lone tooltip groups itself. |
| `Input`, `Label` | [`input`](https://pixilui.com/components/input) | Replaces the input, its label and the help line: `label`, `description` and `error` are props and are wired to the field for screen readers. `prefix`, `suffix` and `clearable` replace hand-built adornments. |

The AI parts of a screen usually have no shadcn/ui equivalent: an approval step before the agent acts, the steps of a run, tool calls, a thinking line, a streaming answer. See the [AI Elements guide](https://pixilui.com/docs/migrate-from-ai-elements) if you built those with AI Elements, or start from [components](https://pixilui.com/components).

## Steps

### 1. Add the registry and the foundation

```bash
npx pixilui@latest init
```

This adds `@pixilui` to `registries` in `components.json` next to your other entries, installs the foundation and writes the agent rules. Your `components/ui/` folder is not touched.

### 2. Add the items the screen needs

```bash
npx pixilui@latest add button dialog input
```

### 3. Swap the imports on one screen

```tsx
// Before
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle, DialogTrigger } from "@/components/ui/dialog";

// After
import { Button } from "@/components/pixilui/button";
import { Dialog, DialogActions, DialogDescription, DialogTitle } from "@/components/pixilui/dialog";
```

A search for `@/components/ui/button` in that screen's folder finds every place to change.

### 4. Change the props

```tsx
// Before
<Dialog>
  <DialogTrigger asChild>
    <Button variant="destructive">Delete agent</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogTitle>Delete the Billing agent?</DialogTitle>
    <DialogDescription>Its 214 runs and 3 schedules go with it.</DialogDescription>
    <DialogFooter>
      <Button variant="outline">Cancel</Button>
      <Button variant="destructive" disabled={pending} onClick={remove}>Delete agent</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

// After
<Dialog variant="danger" trigger={<Button variant="secondary">Delete agent</Button>}>
  <DialogTitle>Delete the Billing agent?</DialogTitle>
  <DialogDescription>Its 214 runs and 3 schedules go with it.</DialogDescription>
  <DialogActions confirm="Delete agent" onConfirm={remove} />
</Dialog>
```

`remove` returns a promise; the dialog shows the pending state, closes on success and shows the error on failure. Drop the `pending` state and the `disabled` flag you kept for it.

### 5. Check the tokens around it

Classes you wrote around the components keep working, but replace shadcn/ui theme classes on the same surface with the PixilUI roles: `bg-card` becomes `bg-surface`, `text-muted-foreground` becomes `text-secondary`, `border` stays `border-border`. In a shadcn/ui app, `bg-accent` is the grey hover of menus; the PixilUI brand fill is `bg-accent-solid`.

### 6. Remove what nothing imports

When no file imports `components/ui/dialog` any more, delete it. Keep the shadcn/ui components you still use, such as tables, forms and menus; they are fine next to PixilUI.

## With an agent

Paste this into Claude Code, Cursor or Codex:

```text
Read https://pixilui.com/docs/migrate-from-shadcn-ui/markdown. Move the settings screen from shadcn/ui Button, Dialog and Input to PixilUI using that mapping, one file at a time, and list what you changed.
```
