Skip to content

Upgrading to 5.0

How to take a Candor UI 4.x project to 5.0: the commands, then every change 5.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 5.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 5.0 changes for the items in candor-lock.json, with the merges the update would make, and writes nothing.

    Look first
  2. 02: Update

    Updates every item, merging your changes into Candor's as update always has. If candor-lock.json still records the layout CLI 2.0.0 nested (candor/candor/), CLI 3 stops and names npx candor-ui@2 update --fold, which folds it first.

    Update
  3. 03: Run the codemods

    Runs, in this order, those of 5.0's codemods (types-data, props, strings-keys, headings) 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@3 migrate <codemod> runs one anyway. They change your own files only: Candor's installed files come with the update, and in one you've changed, what a codemod finds is listed. Each says what it changed and lists what it couldn't under "Left for you". headings changes nothing: it lists each heading item whose level 5.0 changes. 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": Tree's and EnvironmentBand's onSelect, which passes the id; a headingLevel where your page or a test needs 4.x's level (or a <HeadingScope> round the section, except round the free PermissionPrompt, which reads none and takes headingLevel={3}); two different separators to choose between (and a Formatter you build yourself, a test double for useFormat, adds separator and join); a removed sentence key to translate again; and an English export used outside a component. Then the ones below marked "By hand" that no codemod lists, which the type check finds. Then type-check, build and run your tests: one that uses toBeDisabled() on a control that waits checks aria-disabled="true" now.

    Finish by hand, then check

Before you start

  • update --to 5.0.0 prints 5.0.0's text. It reads the 5.0.0 snapshot, which stays as it was released, so the entries 5.1.0 corrected (heading-default, strings-separator, strings-sentences, locked-not-disabled and props-active-id-nav, and the migration lines' npx candor-ui@3) print there as they were. This page has the corrected text, and update to 5.1.0 or later prints it.
  • migrate parses your source with the project's TypeScript or, on TypeScript 7 (which has no JavaScript API) or with none, with the TypeScript 5 that CLI 3 brings: there is nothing to install for the run.
  • @candor-ui/react 5.0.0 has the same type names as before (they were already *Data); it drops statusLabel and follows the heading, separator and naming changes below for its free components. Update it with your package manager.
  • A removed string key still compiles. StringOverrides takes any key, so the type check doesn't find an override of a key 5.0 removes: it just isn't read. migrate strings-keys moves it or lists it, for a strings object it can see (on LocaleProvider or CandorProvider, in a CandorConfig, or typed StringOverrides).
  • Stored strings are data, not source. No codemod reaches translations kept in a database or a CMS: move a stored *Separator override to locale.separator, and translate a removed sentence key's replacement, once, where it is stored.
  • 4.x stays installable. https://candor.themethodic.com/r/v/4.5.0/<name>.json serves every item as 4.5.0 shipped it, with the same licence check, and npx candor-ui add <item> --version 4.5.0 installs from there.
  • A 3.x project goes to 4.0 first, with CLI 2: npx candor-ui@2 update --to 4.0.0, then npx candor-ui@2 migrate 4.0.0 (Upgrading to 4.0). CLI 3 doesn't change a 3.x project; it names those commands. @2 stays on npm for 3.x and 4.x.

What changes (14)

types-data

The six data types are named for what they hold: Message and ToolCall (lib/candor/chat), ApprovalRequest and ResultTable (lib/candor/work), Finding (lib/candor/data) and RunReceipt (lib/candor/ops) are MessageData, ToolCallData, ApprovalRequestData, ResultTableData, FindingData and RunReceiptData, as @candor-ui/react already named them. The old names aren't kept.

On
chat, work, data, ops
What breaks
An import of an old name from lib/candor fails the type check (has no exported member 'Message'). Imports from @candor-ui/react don't change.
Shows in
the type check, imports
What to do
npx candor-ui@3 migrate types-data renames each import and its uses in your files; an import you aliased (Message as M) keeps its local name. It lists a re-export from a file of yours, and a file that already has its own MessageData, for you to change.
Codemod
npx candor-ui@3 migrate types-data, which migrate 5.0.0 runs.

heading-default

Every item that draws a heading takes headingLevel (1 to 6) and, without it, draws its top heading one level below the nearest HeadingScope, else at <h2>, so a page's outline skips no level. Dialogs, sheets, drawers, alert dialogs, settings sections, dashboard tiles and artifact panels set a scope over their content. PageHeader and EmailLayout stay at <h1>; the free PermissionPrompt and PageState default to <h2> and read no scope.

On
artifact-panel, app-layout, dock-window, settings-layout, mcp-connect, model-key, stop-all, entity-disambiguation, permission-prompt, thread-header, form-error-summary, dialog, sheet, drawer, alert-dialog, data-table, reorderable-list, board, details-list, reconcile-table, column-mapper, finding, narrative-block, portfolio-card, report-sheet, monitor-card, run-log, run-estimate, failure-clusters, review-queue, dashboard-tile, approval-request, result-card, plan, environment-promotion, blast-radius, agent-screen, checkpoint-list, clarifying-questions, change-set-review, document-progress-queue, file-list, proposal, workflow-editor, comment-thread, drift-change, ship-review, run-recovery, knowledge-sources, memory-manager, prose-diff, retrieval-inspector, version-history, away-digest, escalation, extraction-review, api-keys, channel-setup, data-region, data-retention, device-sessions, environment-connect, environment-list, integration-connect, schedule-editor, schedule-list, sso-setup, webhooks, account-create, consent-ceremony, invite-chooser, setup-checklist, sign-in, terms-consent, billing-details, invoice-history, plan-gate
What breaks
Where you don't pass headingLevel, a heading can be a different level than in 4.x: most that were <h3> or <h4> are <h2> outside a scope. It looks the same; a test or an outline check that expects 4.x's level sees the change.
Shows in
what is drawn
What to do
npx candor-ui@3 migrate headings lists each use without headingLevel, with the level it drew in 4.x, and changes nothing. Where your page or a test needs that level, pass headingLevel={n}; or wrap a section in <HeadingScope level={n}> (lib/candor/heading-scope, Pro) so the items in it sit one level below. The free PermissionPrompt draws <h2> by default since 5.0 and reads no HeadingScope: pass headingLevel={3} to keep 4.x's <h3>.
Codemod
npx candor-ui@3 migrate headings, which migrate 5.0.0 runs.

heading-level-type

headingLevel is typed HeadingLevel (1 to 6) on every item, where nine props took 2 | 3 | 4 | 5 | 6. lib/candor/heading-level exports the type, and layout/heading still does.

On
environment-list, dashboard-tile, board, data-table, reorderable-list, monitor-card, narrative-block, portfolio-card
What breaks
Nothing you pass stops type-checking. A type of yours read from the prop (ComponentProps<typeof DataTable>["headingLevel"]) now includes 1.
Shows in
the type check
What to do
Nothing for a level you pass. Where you type a level of your own, import HeadingLevel from lib/candor/heading-level.
Codemod
None needed for a level you pass. By hand: import HeadingLevel where you type a level of your own.

props-active-id-nav

TabBar's and NavRail's current, AppSidebar's activeNav and SettingsLayout's active are activeId, the name Candor's other lists use for the id of the item that's selected.

On
tab-bar, nav-rail, app-sidebar, settings-layout
What breaks
The old prop fails the type check; in code that isn't type-checked, nothing shows as current.
Shows in
the type check, what is drawn
What to do
npx candor-ui@3 migrate props renames them on Candor's components; the value is the same. Stepper's, PlanPicker's and InlineSuggestion's current mean something else and stay.
Codemod
npx candor-ui@3 migrate props, which migrate 5.0.0 runs.

props-label-name

SettingsLayout's and DocumentViewer's title, which is their accessible name and isn't drawn, is label; title is Candor's word for a heading that is drawn.

On
settings-layout, document-viewer
What breaks
title fails the type check; in code that isn't type-checked, the settings region or the viewer has no name for a screen reader.
Shows in
the type check, behaviour
What to do
npx candor-ui@3 migrate props renames it on both.
Codemod
npx candor-ui@3 migrate props, which migrate 5.0.0 runs.

props-popup-class

PersonMenu's and ScopeSwitcher's layerClass prop, the classes for the menu's popup, is popupClassName; layerClass is the name of foundation/layer's constant.

On
person-menu, scope-switcher
What breaks
layerClass fails the type check; in code that isn't type-checked, the popup loses your classes.
Shows in
the type check, styling
What to do
npx candor-ui@3 migrate props renames it on both.
Codemod
npx candor-ui@3 migrate props, which migrate 5.0.0 runs.

on-select-id

Tree's and EnvironmentBand's onSelect passes the id of what was chosen, as RunTree, SessionList, Plan and Candor's other lists do, not the node or the environment.

On
tree, environment-band
What breaks
A handler that takes the object fails the type check; in code that isn't type-checked, it reads properties of a string.
Shows in
the type check, behaviour
What to do
Write onSelect={(id) => …} and look the node or the environment up by id where the handler needs it. npx candor-ui@3 migrate props lists each onSelect on them with its file and line, and the type check finds the rest.
Codemod
By hand: no codemod can make this change safely.

strings-compat-exports

The English exports kept for compatibility are removed: statusLabel (lib/candor/status), environmentLabel (lib/candor/team), approvalStateLabel (approval-request), reviewLevels (review-ladder) and standardCards (standard-cards). SessionList's statusLabel prop is a different thing and stays.

On
status, team, approval-request, review-ladder, standard-cards
What breaks
An import of one fails the type check and the build.
Shows in
the type check, imports
What to do
npx candor-ui@3 migrate strings-keys rewrites standardCards used inside a component to const standardCards = useStandardCards() at its top, and lists the others with what to use: useStatusLabel(), EnvironmentBand's strings (production, nonProduction) joined by useFormat().join, useStrings("approvalRequest", approvalRequestStrings) and useStrings("reviewLadder", reviewLadderStrings). Each follows the locale, where the export was English.
Codemod
npx candor-ui@3 migrate strings-keys, which migrate 5.0.0 runs.

chart-strings

BarChart's byCategory, LineChart's over and chartValues and Heatmap's heatmap strings, which nothing has read since 4.2, are removed.

On
bar-chart, line-chart, heatmap
What breaks
An override of one is left in place and isn't read, as it hasn't been since 4.2, so nothing draws differently. It still compiles: StringOverrides takes any key.
Shows in
what is drawn
What to do
npx candor-ui@3 migrate strings-keys deletes the overrides.
Codemod
npx candor-ui@3 migrate strings-keys, which migrate 5.0.0 runs.

strings-separator

The " · " between parts is one string, locale.separator, read through useFormat().separator and useFormat().join, where eleven items each had a key for it (message.metaSeparator, composer.hintSeparator, reasoning.summarySeparator …) and about forty typed it. SubscoreCard's separator and TableScrubberMap's kindSeparator, which joined lists, are removed: a list follows the locale's list format (useFormat().list). Strings that held the separator at their start or in their middle no longer do (integrationConnect.connectedSince is since {when}, membersTable.invitedBy is Invited by {name}, columnMapper.suggested is Suggested, with percentSure beside it, and the others the changelog lists): Candor joins the parts. Four that held it between parts are removed: reconcileTable.summary (now openCount, resolvedCount and matchedCount), evalScorecard.passedFailed (passedCount, failedCount), reviewQueue.progress (position, gradedCount) and dataTable.aggregateLine (nothing to translate).

On
locale, message, composer, permission-prompt, reasoning, payload, search-trace, change-set-review, task-tray, workflow-editor, review-queue, subscore-card, table-scrubber-map, integration-connect, subscription-status, members-table, away-digest, escalation, blast-radius, environment-promotion, clarifying-questions, composer-scope-picker, app-sidebar, inbox, session-list, agent-roster, reconcile-table, eval-scorecard, data-table, column-mapper
What breaks
An override of a removed key isn't read any more: the separator is whatever locale.separator says. The override still compiles, since StringOverrides takes any key, so migrate strings-keys is what finds it. An object you build as a Formatter yourself (a test double for useFormat) fails the type check: it has no separator or join.
Shows in
what is drawn, the type check
What to do
npx candor-ui@3 migrate strings-keys moves an override of one of the eleven keys to strings.locale.separator and removes the old keys; where two give different values, it leaves them and lists them for you to choose one. It lists an override of subscoreCard.separator or tableScrubberMap.kindSeparator, which has nothing to move to, and of reconcileTable.summary, evalScorecard.passedFailed, reviewQueue.progress or dataTable.aggregateLine, with the keys that say it now. An override of a string that no longer holds the separator, written with its own " · ", shows it twice: take the mark out. The codemod lists one that still has it. clarifyingQuestions.before ("Before I plan") is new, the words clarifyingQuestions.progress held before its separator: a translated app translates it, since no codemod lists it. Formatter has two new members, separator and join: an object you build as a Formatter yourself (a test double for useFormat) adds them, or it fails the type check.
Codemod
npx candor-ui@3 migrate strings-keys, which migrate 5.0.0 runs.

strings-sentences

Sentences that were built by joining strings and numbers in code are one string each, with {placeholders}, so a translation can put the words in its own order; numbers go through the locale's formatter. The keys they replace are removed: scheduleEditor.announceNext (say it with announceRule and announceRuleAlone) and the others the changelog lists by item. Some keys stay but no longer fill a placeholder, since Candor joins that part on with locale.separator: environmentPromotion.requestedBy, promoted and declined, and escalation.sentLine and acknowledged ({when}); blastRadius.group and awayDigest.group ({count}); taskTray.started ({status}); and membersTable.invitedBy ({expires}, now expiresIn). Five sentences have new keys that replace none, so nothing is removed: modelKey.keyFor, progressButton.measuredFailed and failedWithFiles, runTree.verdictNote and presence.hereStale.

On
schedule-editor, data-retention, environment-connect, subscription-status, invite-chooser, dual-listbox, subscore-card, freshness-stamp, memory-manager, blast-radius, change-set-review, document-progress-queue, drift-change, environment-promotion, log-stream, plan, ship-review, workflow-editor, escalation, away-digest, task-tray, members-table, composer, composer-scope-picker, search-trace, app-sidebar, inbox, session-list, model-key, progress-button, run-tree, presence
What breaks
An override of a removed key isn't read any more: the item says the sentence in English until you translate its new key. Nothing fails to compile, since StringOverrides takes any key, so migrate strings-keys is what lists it. An override of a kept key that still has its dropped placeholder shows it as written ({when} on screen), with its own " · " beside Candor's. A translated app also says the five new keys' sentences in English (ModelKey's field label for one provider, ProgressButton's description and announcement with failed files, RunTree's verdict note, Presence's stale summary) until you translate them; no codemod lists those, since no key was removed.
Shows in
what is drawn
What to do
npx candor-ui@3 migrate strings-keys lists each override of a removed key with the key that says the sentence now and its placeholders. Translate that key: your words can't be moved for you, since the sentence is different. It also lists an override of a kept key that still has its dropped placeholder or a " · ": take them out. Translate the five new keys too, where your app is translated.
Codemod
npx candor-ui@3 migrate strings-keys, which migrate 5.0.0 runs.

card-deck-backspace

CardDeck no longer dismisses a card on Backspace; Delete still does, and the hint names Delete and the arrow keys. A dismissal is announced only once the app has answered: on the redraw after onDismiss, or when the promise onDismiss returns settles; a card still in items then was refused, and isn't announced when it leaves later.

On
card-deck
What breaks
Backspace on a card does nothing. An app that takes the card out later, after an await, without returning that promise from onDismiss, has its dismissal go unannounced, where 4.x announced it when the card left.
Shows in
behaviour
What to do
Where your own help names Backspace for CardDeck, name Delete. Where onDismiss saves before it takes the card out, return the promise (an async handler does).
Codemod
None needed: it changes what a key does, not your code.

locked-not-disabled

A control that waits on something (a save on its way, a condition not yet met) stays focusable and is aria-disabled, with the reason, through Locked, where it was natively disabled; a press does nothing until it can be used. A ToolCall or SearchTrace row that has finished with nothing behind it, which can never open, is text rather than a button.

On
session-expired, webhooks, api-keys, data-controls, schedule-editor, account-create, profile-settings, terms-consent, invite-members, invite-chooser, step-walker, sign-in, step-up-dialog, tool-call, search-trace, permission-prompt, composer, message-branches, approval-request, ship-review, clarifying-questions, steer-composer, restore-dialog, plan, type-to-confirm, source-ingest, memory-manager, memory-mode, data-import, import-review, billing-details, plan-picker
What breaks
A test that checks such a control with toBeDisabled() fails, since it isn't disabled, and one that finds a finished ToolCall or SearchTrace row with no details as a button finds none. For a person nothing is pressed that wasn't before, and the control can be reached and says why it waits. A translated app says those reasons in English until it translates their keys (50 locked… keys, on 31 of these items: RestoreDialog's reasons were keys already, or are the app's).
Shows in
behaviour, what is drawn
What to do
In a test, check aria-disabled="true" (toHaveAttribute("aria-disabled", "true")) in place of toBeDisabled(), and find a finished ToolCall or SearchTrace row with no details by its text. Nothing changes in the app's code. A translated app translates the reasons the waiting controls give, which are new string keys and say their English words until it does: accountCreate.lockedCreating, apiKeys.lockedCreating, approvalDecision.lockedRecording, approvalRequest.lockedEditChecking, approvalRequest.lockedEditInvalid, approvalRequest.lockedRecording, billingDetails.lockedSaving, clarifyingQuestions.lockedSending, clarifyingQuestions.lockedUnanswered, composer.lockedEmpty, composer.lockedHeld, composer.lockedSending, composer.lockedStreaming, composer.lockedUnavailable, dataControls.lockedPreparing, dataImport.lockedReading, importReview.lockedLoading, inviteChooser.lockedChoose, inviteChooser.lockedJoining, inviteMembers.lockedSending, memoryManager.lockedNoMemory, memoryMode.lockedSaving, messageBranches.lockedFirst, messageBranches.lockedLast, permissionPrompt.lockedBusy, permissionPrompt.lockedInvalid, permissionPrompt.lockedUnanswered, plan.lockedAllSkipped, plan.lockedChoose, planPicker.lockedChoose, planPicker.lockedUnchanged, profileSettings.lockedClean, profileSettings.lockedSaving, scheduleEditor.lockedSaving, searchTrace.lockedNoDetail, sessionExpired.lockedSigningIn, shipReview.lockedShipping, signIn.lockedBusy, signIn.lockedNeedsCode, signIn.lockedNeedsEmail, sourceIngest.lockedNoLink, sourceIngest.lockedNoText, steerComposer.lockedEmpty, stepUpDialog.lockedChecking, stepWalker.lockedAhead, termsConsent.lockedRecording, toolCall.lockedNoDetail, typeToConfirm.lockedWorking, webhooks.lockedAdding and webhooks.lockedTesting.
Codemod
None needed in the app's code. By hand: a test that uses toBeDisabled() on one of these controls checks aria-disabled="true", and a translated app translates the new locked… keys listed above.

cli-3

CLI 3.0.0 (npx candor-ui@3) takes a project to 5.0, with migrate 5.0.0, and works with Candor 4.0 and later. It no longer takes a 3.x project, folds the layout CLI 2.0.0 nested (update --fold), or records items installed before the lockfile (update --from); npx candor-ui@2 stays on npm for those.

On
every item that installs at the default paths: components, hooks, libs, primitives and presets
What breaks
npx candor-ui@2 doesn't have 5.0's codemods. npx candor-ui@3 stops on a 3.x project, and on a lockfile that records CLI 2.0.0's nested layout, naming the @2 command to run.
Shows in
behaviour
What to do
Use npx candor-ui@3 from 5.0. A 3.x project goes to 4.0 first with npx candor-ui@2 update --to 4.0.0 and npx candor-ui@2 migrate 4.0.0; a lockfile with the nested layout is folded with npx candor-ui@2 update --fold first.
Codemod
None needed: run the commands above with npx candor-ui@3.