Skip to content

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.

The commands

  1. 01: 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.

    Look first
  2. 02: 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.

    Update
  3. 03: 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.

    Run the codemods
  4. 04: 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.

    Finish by hand, then 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). 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.