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
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 incandor-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 first02: Update
Installs
@base-ui/reactwith your package manager, whenpackage.jsondoesn't list it (--no-depsnames the command instead), moves each renamed preset's file (github-kinds.tstogithub-preset.ts…) and points your imports at it, then updates every item, merging your changes into Candor's.components/ui/stays yours, and so doesradix-uiif your own components use it.Update03: 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 nocandor-lock.json);npx candor-ui@2 migrate <codemod>runs one anyway. They change Candor's components only: shadcn's own components incomponents/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-runto look first.Run the codemods04: Finish by hand, then check
Make the changes
migratelists under "Left for you": DictationButton'slockedreason, CommandPalette'sfinalFocus, and a file that sets both--series-Nand--chart-N. Then the ones below marked "By hand" that no codemod lists, which the type check finds:composer.onChangeread from an adapter's hook, useDictation'sonChangeoption, and a Radix focus or dismiss handler on one of Candor's primitives; adata-stateselector of yours on them is found by searching your styles. Then type-check and build.theme checkreports a token name 4.0 renamed that is still in your CSS as an error, namingmigrate tokens.Finish by hand, then check
Before you start
update, when it moves files, andmigrateread 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 theradix-uipackage stay; Candor's Base UI primitives sit beside them incomponents/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>.jsonserves every item as 3.0.0 shipped it, with the same licence check, andnpx candor-ui add <item> --version 3.0.0installs 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, thennpx 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: acomponents(orlib) alias that is already a folder namedcandor(@/candor) isn't nested again, a file whose movecandor-lock.jsonrecords stays where you put it and a new stylesheet goes beside it, andmigrate 4.0.0lists 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
asChildon 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
renderand give its children to the part; a Button that renders a link, a span or a div also takesnativeButton={false}.npx candor-ui migrate as-childdoes it on Candor's components only: shadcn's own components in components/ui and anasChildon anything else stay as they are. It lists anasChildit can't follow (no element child, or a value that isn'ttrue). - Codemod
npx candor-ui@2 migrate as-child, whichmigrate 4.0.0runs.
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 understrings.metadataDiffaren't read, so ChangeDiff shows its own.- Shows in
- the type check, what is drawn
- What to do
- Rename the
metadataDiffkey tochangeDiffin the strings you give LocaleProvider or CandorProvider, and importchangeDiffStrings.npx candor-ui migrate strings-keysdoes both. - Codemod
npx candor-ui@2 migrate strings-keys, whichmigrate 4.0.0runs.
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 propsdoes it on Candor's components only. DataTable'sstate={{ kind: "loading" }}is unchanged. - Codemod
npx candor-ui@2 migrate props, whichmigrate 4.0.0runs.
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
onChangeon these fails the type check; in code that isn't type-checked the callback is never called. Code that readscomposer.onChangefrom 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 propsdoes it on Candor's components only: anonChangeon an element of your own, such as an<input>, is a DOM event and stays. By hand:composer.onChangefrom an adapter's hook iscomposer.onValueChange, and useDictation'sonChangeoption isonValueChange. NotificationPreferences' and ToolPermissions'onChange, which report one row's change, are unchanged. - Codemod
npx candor-ui@2 migrate props, whichmigrate 4.0.0runs.
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
disabledon 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}withlocked={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.0lists 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 propsdoes it on Candor's components only. - Codemod
npx candor-ui@2 migrate props, whichmigrate 4.0.0runs.
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
labelon 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 propsdoes it on Candor's components only. The words given understrings.promptSuggestions.labelare unchanged. - Codemod
npx candor-ui@2 migrate props, whichmigrate 4.0.0runs.
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.
onCloseAutoFocuson 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:
delayDurationisdelay,skipDelayDurationistimeout,disableHoverableContentisdisableHoverablePopup;closeDelayis new.npx candor-ui migrate propsdoes it in atooltip: { … }object in any file that imports Candor's CandorProvider or CandorConfig. By hand: replace CommandPalette'sonCloseAutoFocus={(event) => { event.preventDefault(); el.focus() }}withfinalFocus={() => el}(returnfalseto leave focus where you put it); migrate lists each one under "Left for you". - Codemod
npx candor-ui@2 migrate props, whichmigrate 4.0.0runs.
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
*-kindsfile stop resolving once the file has moved. - Shows in
- imports
- What to do
npx candor-ui update --to 4.0.0moves each preset's file to its new name (github-kinds.ts to github-preset.ts …) and points your imports at it (--dry-runlists the moves first). Nothing else changes.- Codemod
- None needed:
npx candor-ui@2 update --to 4.0.0moves each preset's file to its new name and points your imports at it (--dry-runlists 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-Nno longer reaches Candor's charts; one set as--chart-Nnow 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 tokensdoes it in your stylesheets and in the strings of your source (arbitrary classes such asbg-[var(--sunken)], styles); Candor's own stylesheets come with the update. A file that sets both--series-Nand--chart-Nis listed for you to choose the colour.npx candor-ui theme checkreports an old name as an error.migrate tokenscan'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, andmigrate tokens --dry-runlists every rename it would make. - Codemod
npx candor-ui@2 migrate tokens, whichmigrate 4.0.0runs.
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
Directionthrough Candor's locale file no longer finds it there and fails the type check. Imports ofTextDirectionanddirectionOfare unchanged. - Shows in
- imports, the type check
- What to do
- Keep importing
TextDirectionanddirectionOffrom Candor's locale file. If your own code uses Radix'sDirection, import it fromradix-uithere. - 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-stateon them stops matching. A project without@base-ui/reactinstalled doesn't build. - Shows in
- the type check, styling, behaviour
- What to do
npx candor-ui update --to 4.0.0installs@base-ui/react.npx candor-ui migrate propsrenamesdelayDuration,skipDelayDurationanddisableHoverableContenton Candor's TooltipProvider. By hand: move a focus handler toinitialFocusorfinalFocus(an element, a ref or a function that returns one), move an Escape or outside-click handler into the root'sonOpenChangeand calldetails.cancel()where you prevented the default, and changedata-[state=open]and the otherdata-stateselectors 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, whichmigrate 4.0.0runs.