# Upgrading to 4.0

How to take a Candor UI 3.x project to 4.0: the commands, then every change 4.0 makes with what breaks, where it shows, what to do and the codemod that does it, or the edit you make by hand. The same list is in each item's registry entry and is what `candor-ui update --to 4.0.0` prints. The release's notes are in the changelog: https://candor.themethodic.com/changelog#4-0-0.

## The commands

### 1. Look first

From the app's folder (the one with `components.json`), with your work committed. This prints what 4.0 changes for the items in `candor-lock.json`, with the files the update would move (the presets' new names), the npm packages it would install (`@base-ui/react`) and the merges it would make, and writes nothing.

```bash
npx candor-ui@2 update --to 4.0.0 --dry-run
```

### 2. Update

Installs `@base-ui/react` with your package manager, when `package.json` doesn't list it (`--no-deps` names the command instead), moves each renamed preset's file (`github-kinds.ts` to `github-preset.ts` …) and points your imports at it, then updates every item, merging your changes into Candor's. `components/ui/` stays yours, and so does `radix-ui` if your own components use it.

```bash
npx candor-ui@2 update --to 4.0.0
```

### 3. Run the codemods

Runs, in this order, those of 4.0's codemods (`as-child`, `props`, `strings-keys`, `tokens`) that an entry on one of your installed items names, and lists the ones it leaves out (all of them run where there's no `candor-lock.json`); `npx candor-ui@2 migrate <codemod>` runs one anyway. They change Candor's components only: shadcn's own components in `components/ui/` and your own elements stay as they are. Each says what it changed and lists what it couldn't under "Left for you". Add `--dry-run` to look first.

```bash
npx candor-ui@2 migrate 4.0.0
```

### 4. Finish by hand, then check

Make the changes `migrate` lists under "Left for you": DictationButton's `locked` reason, CommandPalette's `finalFocus`, and a file that sets both `--series-N` and `--chart-N`. Then the ones below marked "By hand" that no codemod lists, which the type check finds: `composer.onChange` read from an adapter's hook, useDictation's `onChange` option, and a Radix focus or dismiss handler on one of Candor's primitives; a `data-state` selector of yours on them is found by searching your styles. Then type-check and build. `theme check` reports a token name 4.0 renamed that is still in your CSS as an error, naming `migrate tokens`.

```bash
npx tsc --noEmit && npm run build && npx candor-ui@2 theme check
```

## Before you start

- `update`, when it moves files, and `migrate` read your source with the project's own TypeScript 5. On TypeScript 7, which has no JavaScript API yet, install 5 for the run and put 7 back after; candor-ui never installs it itself.
- **A Radix app keeps its own primitives.** shadcn's components in `components/ui/` and the `radix-ui` package stay; Candor's Base UI primitives sit beside them in `components/candor/ui/`, so the app carries both libraries. In a test Next.js app, six Radix primitives were 57.4 KB of client JavaScript (gzip), and the same six on Base UI added 68.3 KB. A Candor dialog, popover, select or tooltip opened inside or beside the app's own Radix dialog takes the pointer, focus and Escape, which the release checked in Chromium and WebKit.
- **Stored styles are data, not source.** No codemod reaches a theme saved in a database or a CMS: rename its tokens once, where it is stored.
- **3.x stays installable.** `https://candor.themethodic.com/r/v/3.0.0/<name>.json` serves every item as 3.0.0 shipped it, with the same licence check, and `npx candor-ui add <item> --version 3.0.0` installs from there.
- **A 2.x project goes to 3.0 first**, with CLI 1 (1.0.1 or later): `npx candor-ui@1 update --to 3.0.0`, then `npx candor-ui@1 migrate 3.0.0` ([Upgrading to 3.0](https://candor.themethodic.com/upgrading-to-3)). CLI 2.0 doesn't change a 2.x project; it names those commands.
- **Use CLI 2.0.1 or later** (`npx candor-ui@2`). It carries 1.0.1's fixes: a `components` (or `lib`) alias that is already a folder named `candor` (`@/candor`) isn't nested again, a file whose move `candor-lock.json` records stays where you put it and a new stylesheet goes beside it, and `migrate 4.0.0` lists the codemods it leaves out.

## What changes (13)

### as-child

Candor's primitives take `render`, not `asChild`; `<X asChild><a …/></X>` becomes `<X render={<a …/>}/>`: a part that renders as another element is given that element, as in `<Button render={<a href="/docs" />}>Read the docs</Button>`.

- **On:** `accordion`, `alert-dialog`, `avatar`, `badge`, `breadcrumb`, `button`, `checkbox`, `collapsible`, `context-menu`, `dialog`, `drawer`, `dropdown-menu`, `hover-card`, `label`, `menubar`, `navigation-menu`, `pagination`, `popover`, `progress`, `radio-group`, `scroll-area`, `select`, `separator`, `sheet`, `slider`, `switch`, `tabs`, `toggle`, `toggle-group`, `tooltip`
- **What breaks:** `asChild` on one of Candor's primitives fails the type check, and the part renders its own element with the child inside it.
- **Shows in:** the type check, what is drawn
- **What to do:** Pass the child element as `render` and give its children to the part; a Button that renders a link, a span or a div also takes `nativeButton={false}`. `npx candor-ui migrate as-child` does it on Candor's components only: shadcn's own components in components/ui and an `asChild` on anything else stay as they are. It lists an `asChild` it can't follow (no element child, or a value that isn't `true`).
- **Codemod:** `npx candor-ui@2 migrate as-child`, which `migrate 4.0.0` runs.

### strings-change-diff

ChangeDiff's strings take its name: the `metadataDiff` namespace is `changeDiff`, and the `metadataDiffStrings` table is `changeDiffStrings`.

- **On:** `change-diff`
- **What breaks:** `import { metadataDiffStrings }` fails the type check, and words given under `strings.metadataDiff` aren't read, so ChangeDiff shows its own.
- **Shows in:** the type check, what is drawn
- **What to do:** Rename the `metadataDiff` key to `changeDiff` in the strings you give LocaleProvider or CandorProvider, and import `changeDiffStrings`. `npx candor-ui migrate strings-keys` does both.
- **Codemod:** `npx candor-ui@2 migrate strings-keys`, which `migrate 4.0.0` runs.

### props-busy

A component that is waiting says so with `busy`: Combobox's, DataTable's, PromptSuggestions' and ImportReview's `loading`, PermissionMode's and MemoryMode's `pending`, ClarifyingQuestions' `submitting`, WorkflowEditor's `saving`, CommandPalette's `searching`, AssigneePicker's `assigning`, ChangeSetReview's `applying`, ShipReview's `shipping`, DataImport's `reading`, DegradedPanel's `retrying` and ChartFrame's `refreshing` are `busy`.

- **On:** `combobox`, `data-table`, `permission-mode`, `memory-mode`, `prompt-suggestions`, `import-review`, `clarifying-questions`, `workflow-editor`, `command-palette`, `assignee-picker`, `change-set-review`, `ship-review`, `data-import`, `degraded-panel`, `chart-frame`
- **What breaks:** The old name on these fails the type check; in code that isn't type-checked the component stops showing that it's waiting.
- **Shows in:** the type check, behaviour
- **What to do:** Rename the prop to `busy`; it takes the same boolean. `npx candor-ui migrate props` does it on Candor's components only. DataTable's `state={{ kind: "loading" }}` is unchanged.
- **Codemod:** `npx candor-ui@2 migrate props`, which `migrate 4.0.0` runs.

### props-on-value-change

A callback that hands back the component's value is `onValueChange`, as Combobox's and Select's already are: the `onChange` of Composer, DictationButton, PermissionMode, MemoryMode, InstructionsEditor, ModelSettings, ModelPicker, MessageBranches, ColumnMapper, RunScrubber, SoundSetting, CodeEditor and ReviewLadder is `onValueChange`. So are useDictation's `onChange` option and the `onChange` in the `composer` props that useCandorChat, useAgUi and useClaudeAgent return.

- **On:** `permission-mode`, `dictation`, `composer`, `memory-mode`, `instructions-editor`, `model-settings`, `model-picker`, `message-branches`, `column-mapper`, `run-scrubber`, `sound-setting`, `code-editor`, `review-ladder`, `use-candor-chat`, `use-ag-ui`, `use-claude-agent`
- **What breaks:** `onChange` on these fails the type check; in code that isn't type-checked the callback is never called. Code that reads `composer.onChange` from an adapter's hook fails the type check; `<Composer {...composer} />` keeps working.
- **Shows in:** the type check, behaviour
- **What to do:** Rename the prop to `onValueChange`; it gets the same value. `npx candor-ui migrate props` does it on Candor's components only: an `onChange` on an element of your own, such as an `<input>`, is a DOM event and stays. By hand: `composer.onChange` from an adapter's hook is `composer.onValueChange`, and useDictation's `onChange` option is `onValueChange`. NotificationPreferences' and ToolPermissions' `onChange`, which report one row's change, are unchanged.
- **Codemod:** `npx candor-ui@2 migrate props`, which `migrate 4.0.0` runs.

### props-heading-level

Every prop that sets a heading's level is `headingLevel`, and every one takes the same levels, 1 to 6 (`1 | 2 | 3 | 4 | 5 | 6`). EmailPreview took 2 to 4, CommentThread and Finding 2 to 5, ArtifactPanel and ExtractionReview 1 to 4, the rest 2 to 6.

- **On:** `email-layout`, `comment-thread`, `finding`, `artifact-panel`, `extraction-review`, `permission-prompt`, `details-list`, `reconcile-table`, `failure-clusters`, `run-log`, `approval-request`, `change-set-review`, `document-progress-queue`, `file-list`, `proposal`, `workflow-editor`
- **What breaks:** A level you pass today still type-checks: the union only widens. Code of yours that reads the prop's type (`ComponentProps<typeof EmailPreview>["headingLevel"]`) and passes it to something narrower fails the type check.
- **Shows in:** the type check
- **What to do:** Nothing for a level you pass. Widen your own type where the type check asks, or narrow the value before you pass it on. No codemod: there is nothing to rename.
- **Codemod:** None needed for a level you pass. By hand: widen a type of your own where the type check asks.

### props-locked

A control that can't be used takes `locked`, the reason the person is shown, in place of `disabled` (a control that can't be used says why): DictationButton's `disabled` is `locked`.

- **On:** `dictation`
- **What breaks:** `disabled` on DictationButton fails the type check; in code that isn't type-checked the button stays usable.
- **Shows in:** the type check
- **What to do:** Replace `disabled={x}` with `locked={x ? "Wait for the reply to finish." : undefined}`, in your own words. By hand: only you know the reason. `npx candor-ui migrate 4.0.0` lists each place under "Left for you".
- **Codemod:** By hand: no codemod can make this change safely.

### props-active-id

The item a list shows as selected is `activeId`, and the callback that selects one is `onSelect`, as in RunTree, GraphView, VersionHistory, AgentRoster, SessionList, SidePanel and ApprovalQueue: Tree's `selectedId` is `activeId`, and Plan's `activeStepId` and `onSelectStep` are `activeId` and `onSelect`.

- **On:** `tree`, `plan`
- **What breaks:** The old names fail the type check; in code that isn't type-checked nothing shows as selected, and Plan's steps stop being buttons.
- **Shows in:** the type check, behaviour
- **What to do:** Rename the props; they take the same values. `npx candor-ui migrate props` does it on Candor's components only.
- **Codemod:** `npx candor-ui@2 migrate props`, which `migrate 4.0.0` runs.

### props-title

A component's visible heading is `title`, as in DataTable, PageHeader and the charts; `label` is an accessible name or a control's own text. PromptSuggestions' `label`, the heading above the suggestions, is `title`.

- **On:** `prompt-suggestions`
- **What breaks:** `label` on PromptSuggestions fails the type check; in code that isn't type-checked the heading reads "Try asking".
- **Shows in:** the type check, what is drawn
- **What to do:** Rename the prop to `title`; it takes the same text. `npx candor-ui migrate props` does it on Candor's components only. The words given under `strings.promptSuggestions.label` are unchanged.
- **Codemod:** `npx candor-ui@2 migrate props`, which `migrate 4.0.0` runs.

### props-base-ui-names

Candor's own components that still took Radix's names take Base UI's, as the primitives do: CandorConfig's `tooltip` takes `delay`, `closeDelay`, `timeout` and `disableHoverablePopup` (TooltipProvider's names) where it took `delayDuration`, `skipDelayDuration` and `disableHoverableContent`, and CommandPalette's `onCloseAutoFocus` is `finalFocus`, which returns where focus goes.

- **On:** `candor-provider`, `command-palette`
- **What breaks:** A config written with the old keys fails the type check; one that isn't type-checked (JSON, say) has its tooltip timing ignored. `onCloseAutoFocus` on CommandPalette fails the type check, and in code that isn't type-checked it's never called.
- **Shows in:** the type check, behaviour
- **What to do:** Rename the config's keys: `delayDuration` is `delay`, `skipDelayDuration` is `timeout`, `disableHoverableContent` is `disableHoverablePopup`; `closeDelay` is new. `npx candor-ui migrate props` does it in a `tooltip: { … }` object in any file that imports Candor's CandorProvider or CandorConfig. By hand: replace CommandPalette's `onCloseAutoFocus={(event) => { event.preventDefault(); el.focus() }}` with `finalFocus={() => el}` (return `false` to leave focus where you put it); migrate lists each one under "Left for you".
- **Codemod:** `npx candor-ui@2 migrate props`, which `migrate 4.0.0` runs.

### preset-rename

The presets are named for what they hold, kinds and nouns: github-kinds is github-preset (github-preset.ts), and hubspot-kinds, jira-kinds, linear-kinds and salesforce-kinds are hubspot-preset, jira-preset, linear-preset and salesforce-preset. Their exports keep their names.

- **On:** `github-kinds`, `hubspot-kinds`, `jira-kinds`, `linear-kinds`, `salesforce-kinds`, `github-preset`, `hubspot-preset`, `jira-preset`, `linear-preset`, `salesforce-preset`
- **What breaks:** Imports of a `*-kinds` file stop resolving once the file has moved.
- **Shows in:** imports
- **What to do:** `npx candor-ui update --to 4.0.0` moves each preset's file to its new name (github-kinds.ts to github-preset.ts …) and points your imports at it (`--dry-run` lists the moves first). Nothing else changes.
- **Codemod:** None needed: `npx candor-ui@2 update --to 4.0.0` moves each preset's file to its new name and points your imports at it (`--dry-run` lists the moves first).

### tokens-cd-prefix

Candor's own tokens take the `--cd-` prefix, as its motion and sound tokens have: 23 colour roles, from `--sunken` to `--cd-sunken`, `--warning` to `--cd-warning`, `--faint-foreground` to `--cd-faint-foreground`, `--border-strong`, `--primary-hover`, the `-weak` and `-text` pairs, `--on-success`, `--on-destructive`, `--surface-agent`, `--surface-user` and `--text-provisional`. The six chart series join shadcn's: `--series-1` to `--series-5` are `--chart-1` to `--chart-5`, which Candor's charts now read, and `--series-6` is `--cd-chart-6`. shadcn's own tokens (`--background`, `--primary`, `--chart-1` …) keep shadcn's names, and Tailwind classes (`bg-sunken`, `text-warning-text`) don't change.

- **On:** `tokens`, `theme`
- **What breaks:** Nothing fails to compile. A brand file or stylesheet that sets an old name no longer reaches Candor, which shows its own value, and a `var()` of an old name reads nothing. A chart colour set as `--series-N` no longer reaches Candor's charts; one set as `--chart-N` now does, so an app's shadcn chart colours restyle Candor's charts too.
- **Shows in:** styling
- **What to do:** Rename the tokens where you set them and where you read them with `var()`. `npx candor-ui migrate tokens` does it in your stylesheets and in the strings of your source (arbitrary classes such as `bg-[var(--sunken)]`, styles); Candor's own stylesheets come with the update. A file that sets both `--series-N` and `--chart-N` is listed for you to choose the colour. `npx candor-ui theme check` reports an old name as an error. `migrate tokens` can't tell a variable your app defines for itself from Candor's, so it renames those too: if your app had its own `--warning` (or another of these names), rename yours back after migrating. `npx candor-ui theme check`, run before migrating, lists each place your CSS sets or reads one of these names, with its file and line, and `migrate tokens --dry-run` lists every rename it would make.
- **Codemod:** `npx candor-ui@2 migrate tokens`, which `migrate 4.0.0` runs.

### direction-moved

LocaleProvider hands the text direction to Base UI's DirectionProvider, so Candor's primitives follow it; the locale file no longer imports Radix's `Direction`. `TextDirection` and `directionOf` are unchanged.

- **On:** `locale`
- **What breaks:** Code that reached Radix's `Direction` through Candor's locale file no longer finds it there and fails the type check. Imports of `TextDirection` and `directionOf` are unchanged.
- **Shows in:** imports, the type check
- **What to do:** Keep importing `TextDirection` and `directionOf` from Candor's locale file. If your own code uses Radix's `Direction`, import it from `radix-ui` there.
- **Codemod:** By hand: no codemod can make this change safely.

### primitives-base-ui

Every one of Candor's primitives is built on Base UI: `@base-ui/react` replaces `radix-ui`, and the primitives install in components/candor/ui/ as before. Where Radix and Base UI name a prop differently, they take Base UI's: TooltipProvider's `delayDuration` is `delay`, `skipDelayDuration` is `timeout` and `disableHoverableContent` is `disableHoverablePopup`; a dialog's or popover's `onOpenAutoFocus` and `onCloseAutoFocus` are `initialFocus` and `finalFocus`; `onEscapeKeyDown` and `onInteractOutside` are the root's `onOpenChange(open, details)`, where `details.cancel()` keeps it open. Their state shows in Base UI's attributes (`data-open`, `data-closed`, `data-checked` …), not Radix's `data-state`.

- **On:** `accordion`, `alert`, `alert-dialog`, `avatar`, `badge`, `breadcrumb`, `button`, `calendar`, `card`, `carousel`, `checkbox`, `collapsible`, `combobox`, `command`, `context-menu`, `date-picker`, `date-range-picker`, `dialog`, `drawer`, `dropdown-menu`, `field`, `form-error-summary`, `hover-card`, `input`, `kbd`, `label`, `locked`, `menubar`, `navigation-menu`, `number-input`, `one-time-code`, `pagination`, `password-input`, `popover`, `progress`, `radio-group`, `scroll-area`, `select`, `separator`, `sheet`, `skeleton`, `slider`, `stepper`, `switch`, `table`, `tabs`, `textarea`, `time-input`, `toggle`, `toggle-group`, `tooltip`
- **What breaks:** A Radix-only prop on one of Candor's primitives fails the type check, and in code that isn't type-checked it does nothing. A style of yours that selects `data-state` on them stops matching. A project without `@base-ui/react` installed doesn't build.
- **Shows in:** the type check, styling, behaviour
- **What to do:** `npx candor-ui update --to 4.0.0` installs `@base-ui/react`. `npx candor-ui migrate props` renames `delayDuration`, `skipDelayDuration` and `disableHoverableContent` on Candor's TooltipProvider. By hand: move a focus handler to `initialFocus` or `finalFocus` (an element, a ref or a function that returns one), move an Escape or outside-click handler into the root's `onOpenChange` and call `details.cancel()` where you prevented the default, and change `data-[state=open]` and the other `data-state` selectors to Base UI's attributes. A Radix app keeps its own components/ui and the radix-ui package; Candor's primitives sit beside them in components/candor/ui.
- **Codemod:** `npx candor-ui@2 migrate props`, which `migrate 4.0.0` runs.
