Migrate from Components 3 to 4
Components 4 replaces the PrimeReact-backed Components 3 foundation with Components-owned markup, styling contracts, and public types. React Aria supplies selected interaction primitives internally. The current Components 4 manifest does not declare PrimeReact, PrimeIcons, PrimeUI, or PrimeUI themes as dependencies or peers; applications retaining direct imports keep their own package and license boundaries.
This is a major-version migration. Rendered markup, styling parts, provider configuration, date entry, root imports, and some deprecated props change.
The adapters do not replace complete Components widgets. Core continues to own Dialog, Dropdown, DatePicker, paginator, table, focus, overlay, selection, and keyboard behavior; an adapter only presents button, icon-button, text-input, text-area, checkbox, radio, switch, progress, and surface slots. Installing an adapter neither restores PrimeReact public APIs nor transfers key handling to the adapter.
Recommended order, stop points, and rollback
Section titled “Recommended order, stop points, and rollback”- Create a migration branch and preserve the current source, package manifest, and lockfile as the rollback point. Run the existing Components 3 lint, type check, specs, and production build before changing anything.
- While the installed Core package is still in the Components 3 source window (
>=3 <4), preview and applycratis-components-remove-root-namespace-imports. Its output uses subpaths available on both sides of the migration. Re-run the Components 3 gates and checkpoint the import-only change. - Upgrade Core to the bounded Components 4 target (
^4.0.0), mount the Components provider, and import the required stylesheet layers. Do not remove Prime packages, providers, themes, or licenses that still serve a direct Prime island. - Preview and apply the Button transform, then the change-handler transform. These produce Components 4 source and therefore belong after the Core upgrade. Stop after each transform to resolve every diagnostic and
TODO(cratis-codemod), then run the gates and checkpoint the result. - Remove each retained Prime island only after its replacement has passed behavior, accessibility, theme, and license review.
The Components 4 Migrator package accepts either the supported Components 3 source window or Components 4 target window at preflight. That permits a recovery run after upgrading, but it does not change the recommended order above. A failed compatibility preflight scans and writes nothing. A transform refusal exits nonzero and may annotate or migrate other independently safe syntax, so inspect the diff; restore the preceding checkpoint before retrying if an all-or-nothing rollback is required.
Update dependencies
Section titled “Update dependencies”Use the commands for the application’s package manager. Remove only Prime packages that were installed for Components and are no longer owned by a retained direct Prime island.
# npmnpm uninstall primereact primeicons @primereact/core @primereact/headless \ @primereact/hooks @primereact/styles @primereact/types @primeuix/themesnpm install '@cratis/components@^4.0.0'
# pnpmpnpm remove primereact primeicons @primereact/core @primereact/headless \ @primereact/hooks @primereact/styles @primereact/types @primeuix/themespnpm add '@cratis/components@^4.0.0'
# Yarnyarn remove primereact primeicons @primereact/core \ @primereact/headless @primereact/hooks @primereact/styles @primereact/types \ @primeuix/themesyarn add '@cratis/components@^4.0.0'Keep a Prime package only when your application still imports it directly. Migrate those imports separately; Components no longer supplies or requires them.
Applications using Canvas or PivotViewer must install pixi.js@^8.20.0, now an optional peer rather than a nested Components dependency. Align any existing direct Pixi dependency to the same compatible resolution so public PIXI.Container and pointer-event types come from one package instance. Applications using only non-Pixi subpaths do not need it.
The package declares an Arc peer range of >=20.3.1 <23.
Import from explicit subpaths
Section titled “Import from explicit subpaths”The canonical rule going forward: the package root is setup-only; every component ships from its own subpath.
// Before — Components 3 root namespace (removed in Components 4)import { Canvas } from '@cratis/components';
<Canvas.Canvas showControls> <Canvas.CanvasItem x={0} y={0}> Content </Canvas.CanvasItem></Canvas.Canvas>;// After — canonical subpath, named importsimport { Canvas, CanvasItem } from '@cratis/components/Canvas';
<Canvas showControls> <CanvasItem x={0} y={0}> Content </CanvasItem></Canvas>;This is an intentional Components 4 breaking change. The package root now exposes setup APIs only; component namespaces no longer exist there. Every retained namespace maps mechanically: replace import { X } from '@cratis/components' with either an equivalent namespace import from the documented subpath, or named imports from that subpath. The removed renderer-only Compatibility namespace is the deliberate manual exception.
| Removed Components 3 root namespace | Components 4 subpath | Namespace-preserving migration | Named migration |
|---|---|---|---|
Canvas | @cratis/components/Canvas | import * as Canvas from '@cratis/components/Canvas' | import { Canvas, CanvasItem } from '@cratis/components/Canvas' |
Chat | @cratis/components/Chat | import * as Chat from '@cratis/components/Chat' | import { ChatSidebar, ChatConversation } from '@cratis/components/Chat' |
CommandDialog | @cratis/components/CommandDialog | import * as CommandDialog from '@cratis/components/CommandDialog' | import { CommandDialog } from '@cratis/components/CommandDialog' |
CommandStepper † | @cratis/components/CommandDialog (namespace-preserving) | import * as CommandStepper from '@cratis/components/CommandDialog' | import { CommandStepper } from '@cratis/components/CommandStepper' |
CommandForm | @cratis/components/CommandForm | import * as CommandForm from '@cratis/components/CommandForm' | import { AutoCommandForm, InputTextField } from '@cratis/components/CommandForm' |
Common | @cratis/components/Common | import * as Common from '@cratis/components/Common' | import { Button } from '@cratis/components/Common' |
Compatibility †† | No Components 4 subpath | Manual migration required; the codemod refuses and exits nonzero | Replace Prime compatibility contracts with typed Cratis parts, then remove it |
DataPage | @cratis/components/DataPage | import * as DataPage from '@cratis/components/DataPage' | import { DataPage, Column } from '@cratis/components/DataPage' |
DataTables | @cratis/components/DataTables | import * as DataTables from '@cratis/components/DataTables' | import { DataTableForQuery, Column } from '@cratis/components/DataTables' |
Dialogs | @cratis/components/Dialogs | import * as Dialogs from '@cratis/components/Dialogs' | import { Dialog } from '@cratis/components/Dialogs' |
Display | @cratis/components/Display | import * as Display from '@cratis/components/Display' | import { Tag, Badge } from '@cratis/components/Display' |
Dropdown | @cratis/components/Dropdown | import * as Dropdown from '@cratis/components/Dropdown' | import { Dropdown } from '@cratis/components/Dropdown' |
Filter | @cratis/components/Filter | import * as Filter from '@cratis/components/Filter' | import { FilterPanel } from '@cratis/components/Filter' |
Notifications | @cratis/components/Notifications | import * as Notifications from '@cratis/components/Notifications' | import { Toaster, toast } from '@cratis/components/Notifications' |
ObjectContentEditor | @cratis/components/ObjectContentEditor | import * as ObjectContentEditor from '@cratis/components/ObjectContentEditor' | import { ObjectContentEditor } from '@cratis/components/ObjectContentEditor' |
ObjectNavigationalBar | @cratis/components/ObjectNavigationalBar | import * as ObjectNavigationalBar from '@cratis/components/ObjectNavigationalBar' | import { ObjectNavigationalBar } from '@cratis/components/ObjectNavigationalBar' |
PivotViewer | @cratis/components/PivotViewer | import * as PivotViewer from '@cratis/components/PivotViewer' | import { PivotViewer } from '@cratis/components/PivotViewer' |
SchemaEditor | @cratis/components/SchemaEditor | import * as SchemaEditor from '@cratis/components/SchemaEditor' | import { SchemaEditor } from '@cratis/components/SchemaEditor' |
TimeMachine | @cratis/components/TimeMachine | import * as TimeMachine from '@cratis/components/TimeMachine' | import { TimeMachine, EventsView } from '@cratis/components/TimeMachine' |
Toolbar | @cratis/components/Toolbar | import * as Toolbar from '@cratis/components/Toolbar' | import { Toolbar, ToolbarButton } from '@cratis/components/Toolbar' |
Types | @cratis/components/types | import * as Types from '@cratis/components/types' | import { JsonSchema, Json } from '@cratis/components/types' |
† The removed root CommandStepper namespace aliased the entire CommandDialog module. The codemod therefore maps that namespace to @cratis/components/CommandDialog, preserving the correct module identity for members retained in Components 4, including StepperCommandDialog, CommandDialog, CommandStepper, StepperPanel, and applyBeforeExecute. It does not restore an export removed from that module, such as the accidental CommandStepperContent export described below. New code that needs only the standalone component should use the narrower named import from @cratis/components/CommandStepper.
†† Components 3.6 also exported the Compatibility namespace and its pass-through helpers directly from the root. Components 4 intentionally has no destination for Compatibility, assertPrimeReact11PassThroughCompatibility, components3PrimeReact11PassThroughContract, PrimeReact11PassThroughComponent, primeReact11PassThroughSentinelAttribute, or primeReact11PassThroughSentinelPreset. The codemod recognizes these as known removals, leaves their statement unchanged, and exits nonzero with typed-parts guidance; it never misclassifies them as a new setup symbol or invents a subpath.
The Components 3.6 Chat namespace maps to @cratis/components/Chat like every other retained family. Its public models, sidebar, conversation, observable-query wrapper, mentions, and emoji contracts remain on that subpath; only the root namespace import changes.
@cratis/components/CommandForm/fields is the same module as @cratis/components/CommandForm — either subpath resolves identically, so the CommandForm row’s migration applies to both. Provider/configuration/message setup stays on the package root; only Common components such as Button move to @cratis/components/Common.
Run the migration codemod in preview mode first, then apply it:
TOOLING_RANGE='^4.0.0'npx --package "@cratis/components.migrator@$TOOLING_RANGE" \ cratis-components-remove-root-namespace-imports --check path/to/app/srcnpx --package "@cratis/components.migrator@$TOOLING_RANGE" \ cratis-components-remove-root-namespace-imports path/to/app/srcComponents 3 has no matching 3.x Migrator release. Use the bounded Components 4 tooling range (>=4 <5; ^4.0.0 above is the shell-safe equivalent). Never substitute latest. Tooling packages share the repository release version with Core. The bounded range avoids unnecessary exact-patch coupling when invoking an already published 4.x tool. Before scanning or writing, the Migrator validates its bundled compatibility manifest, its own version, and the installed Components package from the invocation directory. It fails closed when Components is absent, outside the supported 3.x source or 4.x target window, or the manifest is invalid.
The examples use npm’s npx. The same bounded package can be launched without adding it to the application:
# pnpmpnpm dlx --package "@cratis/components.migrator@$TOOLING_RANGE" \ cratis-components-remove-root-namespace-imports --check path/to/app/src
# Yarn 2+yarn dlx --package "@cratis/components.migrator@$TOOLING_RANGE" \ cratis-components-remove-root-namespace-imports --check path/to/app/srcQuote any real path containing spaces. Do not type angle-bracket placeholders in a shell: <...> is input-redirection syntax.
The codemod scans JavaScript/JSX and TypeScript/TSX (including .mjs, .cjs, .mts, and .cts), preserves aliases and type-only imports, splits mixed setup/namespace imports, and rewrites a named export { X } from '@cratis/components' re-export the same way as the matching import. It refuses to guess at default imports, whole-package namespace imports, TypeScript import = require(...) assignments, dynamic imports, CommonJS require(...), wildcard or whole-package re-exports (export * from '@cratis/components' / export * as X from '@cratis/components'), side-effect imports, or unknown symbols. Review every reported unsupported case manually, then run the consuming project’s lint, build, and tests.
Migrate Button appearance and change callbacks
Section titled “Migrate Button appearance and change callbacks”Run the root-import codemod above first. The Button and callback codemods resolve Components-owned identifiers from explicit subpaths, so the authoritative order is:
cratis-components-remove-root-namespace-importscratis-components-button-variant-tonecratis-components-change-handler
Use the bounded Components 4 tooling train and preview each transform before applying it:
TOOLING_RANGE='^4.0.0'
npx --package "@cratis/components.migrator@$TOOLING_RANGE" \ cratis-components-button-variant-tone --check path/to/app/srcnpx --package "@cratis/components.migrator@$TOOLING_RANGE" \ cratis-components-button-variant-tone path/to/app/src
npx --package "@cratis/components.migrator@$TOOLING_RANGE" \ cratis-components-change-handler --check path/to/app/srcnpx --package "@cratis/components.migrator@$TOOLING_RANGE" \ cratis-components-change-handler path/to/app/srcKeep the tooling range within >=4 <5; never use latest. The Migrator preflight enforces the bundled source/target support windows.
The Button transform preserves the legacy link → text → outlined → solid precedence and maps literal props to variant, tone, and shape. Existing literal new props win:
// Before<Button outlined rounded severity='warn'>Review</Button><Button variant='ghost' text severity='danger'>Cancel</Button>
// After<Button variant='outline' shape='pill' tone='caution'>Review</Button><Button variant='ghost' tone='critical'>Cancel</Button>secondary and contrast map to neutral; info and help to accent; success to positive; warn to caution; and danger to critical. contrast adds variant='solid' only when no explicit or stronger legacy variant applies.
The callback transform rewrites only structurally-proven single forwarding callbacks on affected Components-owned Dropdown and CommandForm controls:
// Before<Dropdown onChange={(event) => setRole(event.value)} /><InputTextField onChange={(event) => setName(event.target.value)} /><CheckboxField onChange={({ target: { checked } }) => setEnabled(checked)} />
// After<Dropdown onChange={(value) => setRole(value)} /><InputTextField onChange={(value) => setName(value)} /><CheckboxField onChange={(value) => setEnabled(value)} />Dynamic Button props, JSX spreads, unknown new/legacy conflicts, duplicate props, multi-use callbacks, and native-event-dependent callbacks are not guessed. They stay semantically unchanged, produce a nonzero exit, and receive one syntax-safe TODO(cratis-codemod) annotation:
// Manual review required<Button text={appearance.text} severity={appearance.severity} /><Dropdown onChange={(event) => { audit(event.originalEvent); setRole(event.value);}} />Review every diagnostic and TODO(cratis-codemod) before removing the compatibility props. Then run the application’s formatter, lint, type check, tests, and production build. Both codemods are idempotent, include their own TypeScript compiler, support --package, require Node.js 20 or newer, and recursively scan .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, and .cts files.
Install the bounded Components 4 ESLint train after migration and compose its recommended config after @cratis/eslint-config; no-root-barrel-import prevents component namespaces from returning. The ESLint plugin shares the repository release version with Core:
TOOLING_RANGE='^4.0.0'npm install --save-dev "@cratis/eslint-plugin-components@$TOOLING_RANGE"See the @cratis/eslint-plugin-components README included with that package for the flat-config example and the other Components consumer rules.
See UI foundation: Capability profiles for how these subpaths group into Foundation, Advanced React, and Spatial, and the capability matrix for what each profile owns.
Keep the stylesheet entry points
Section titled “Keep the stylesheet entry points”The three Cratis-owned stylesheet entries remain:
import '@cratis/components/tokens';import '@cratis/components/styles';import '@cratis/components/theme'; // optional baseline appearancetokensdefines the stable semantic--cratis-*variables.stylescontains structural rules for every component.themesupplies the optional baseline light/dark values.
A custom product design can omit theme, define the --cratis-* variables itself, and style stable component parts through classes or pt.
Simplify the provider
Section titled “Simplify the provider”The provider now owns locale and Components-specific labels. Unknown renderer keys are a type error so a migrated app cannot silently lose its theme, license, global pass-through, ripple, or z-index behavior. Remove those keys from CratisComponentsProvider and configure any remaining direct Prime provider independently.
import { CratisComponentsProvider } from '@cratis/components';
export const ApplicationRoot = ({ children }: { children: React.ReactNode }) => ( <CratisComponentsProvider value={{ locale: 'nb-NO', messages: { paginator: { navigation: 'Sidenavigasjon', first: 'Første side', previous: 'Forrige side', next: 'Neste side', last: 'Siste side', }, datePicker: { today: 'I dag', clear: 'Tøm', openCalendar: 'Åpne kalender', previousMonth: 'Forrige måned', nextMonth: 'Neste måned', }, toolbar: { label: 'Verktøy', }, }, }} toaster > {children} </CratisComponentsProvider>);locales remains temporarily accepted and maps the old paginator/date labels, but new code should use messages. Renderer keys such as license, theme, defaults, pt, ripple, unstyled, and z-index settings are not part of this provider.
Replace renderer presets with tokens
Section titled “Replace renderer presets with tokens”Remove styledMode(), CratisPreset, and primeReactStyles before upgrading. Components 4 removes three renderer-specific subpaths:
| Removed subpath | Migration |
|---|---|
@cratis/components/styled | Import Cratis tokens/styles and map product tokens directly as shown below. |
@cratis/components/compatibility | Replace Prime slot types/sentinel presets with each component’s Cratis-owned *Parts type. The root Compatibility namespace is also removed. |
@cratis/components/primereact-v10-palette | Remove legacy Prime variable dependencies; define product tokens and map them to --cratis-*, or use the baseline theme. |
There is no compatibility-package replacement in Components 4. Stay on Components 3 while renderer-specific types or selectors remain.
Removed symbol mapping
Section titled “Removed symbol mapping”| Removed Components 3 export | Components 4 action |
|---|---|
styledMode, StyledModeOptions, CratisPreset, primeReactStyles | Remove the renderer configuration. Pass locale/messages to CratisComponentsProvider, import the Cratis stylesheet layers, and map product values directly to --cratis-*. |
primeReactCssLayer, primeReactCssLayerOrder | Delete unless the product still owns direct Prime CSS. Product cascade-layer ordering now belongs in product CSS. |
cratisDarkModeSelector | Use the product’s own theme selector and assign the corresponding --cratis-* values under it. |
assertPrimeReact11PassThroughCompatibility | Delete after migrating every renderer slot to a typed Cratis *Parts surface. Package export verification replaces the old renderer-sentinel check. |
components3PrimeReact11PassThroughContract, PrimeReact11PassThroughComponent | Replace with the component-specific DialogParts, DataTableParts, TablePaginatorParts, StepperParts, ToolbarParts, and related public types. |
primeReact11PassThroughSentinelAttribute, primeReact11PassThroughSentinelPreset | Replace sentinel selectors with documented data-cratis-part and state attributes. |
@cratis/components/primereact-v10-palette variables | Map the product’s canonical tokens directly to --cratis-*; do not preserve a product → Prime → Cratis bridge. |
Before:
<CratisComponentsProvider value={{ license, ...styledMode({ preset: ProductPreset }) }}> <App /></CratisComponentsProvider>After:
<CratisComponentsProvider value={{ locale: 'en-US' }}> <App /></CratisComponentsProvider>Map product tokens directly in CSS:
:root { --cratis-primary-color: var(--product-accent-700); --cratis-primary-color-text: var(--product-text-inverse); --cratis-surface-card: var(--product-surface); --cratis-surface-overlay: var(--product-surface); --cratis-surface-border: var(--product-border); --cratis-text-color: var(--product-text-primary); --cratis-text-color-secondary: var(--product-text-secondary); --cratis-focus-ring: var(--product-focus-ring);}This removes the old product-token → Prime preset → Prime variable → Cratis variable translation chain.
Removed accidental package exports
Section titled “Removed accidental package exports”An audit of the package exports map (#173) found implementation-only symbols that were unintentionally reachable from a public subpath — each was exported only because the owning module’s barrel used a blanket export *, not because it was a supported contract. Components 4 stops re-exporting them from their public barrel; the underlying files keep the symbol for their own internal cross-file use, so this is a package-export change only, not a behavior change.
| Removed export | Subpath(s) | Migration |
|---|---|---|
CommandStepperContent | @cratis/components/CommandStepper, @cratis/components/CommandDialog | Private rendering primitive behind CommandStepper and StepperCommandDialog. Use one of those components; there is no direct public replacement. |
PivotViewerOptimized | @cratis/components/PivotViewer | Was an accidental alias of PivotViewer. Import PivotViewer instead. |
getInitials | @cratis/components/Canvas | Private PersonAvatarCircle helper. Not part of the public API. |
reactionsExcludingUser | @cratis/components/Canvas | Private ChatMessageBubble rendering helper. findOwnReaction remains public for consumers that own reaction commands. |
matchCandidates, activeMentionQuery, applyMention, MentionApplied | @cratis/components/Canvas | Private ChatComposer mention helpers. Not part of the public API. |
EMOJI_CATALOG, EmojiCategoryKey | @cratis/components/Canvas | Private EmojiPicker implementation details. Not part of the public API. |
DEFAULT_EMOJIS, QUICK_ROW_SIZE | @cratis/components/Canvas | Private recentEmojis/rememberEmoji constants. Not part of the public API. |
buildFilterValues, buildRangeValues, RenderedHistogramBucket | @cratis/components/Filter | Private useFilterState/RangeHistogramFilter helpers. Not part of the public API. |
None of these had a documented contract, and none is required by any other public API in this package. An application that imported one of these directly has no documented replacement to migrate to — inline the equivalent logic, or open an issue describing the use case if the behavior should become a supported public contract.
The surfaces this audit confirmed as intentional and kept public — ToastRecord, getToastSnapshot, subscribeToToasts, ToastDispatch, EmojiMemory, ChatAuthorKind, DEFAULT_TYPE_FORMATS, NavigationItem, Json, and TimeMachine’s Properties — are unchanged and now carry TSDoc explaining their contract and, where relevant, their extension-point role.
Migrate pass-through configuration
Section titled “Migrate pass-through configuration”The pt prop remains the per-part customization surface, but its values are now ordinary HTML attributes and its keys are stable Cratis names. ptOptions and unstyled remain accepted temporarily but have no effect: part attributes always merge, and Components always uses consumer-owned CSS.
<Dialog title='Confirm deletion' pt={{ backdrop: { className: 'product-dialog-backdrop' }, root: { className: 'product-dialog' }, header: { className: 'product-dialog-header' }, title: { className: 'product-dialog-title' }, close: { className: 'product-dialog-close' }, content: { className: 'product-dialog-content' }, footer: { className: 'product-dialog-footer' }, }}> This cannot be undone.</Dialog>Every meaningful element also carries data-cratis-part. Interactive states use attributes such as data-selected, data-invalid, data-disabled, data-active, and data-position. Do not target React Aria class names or internal DOM structure.
Common part mappings
Section titled “Common part mappings”| Components 3 renderer slot | Components 4 Cratis part |
|---|---|
Dialog mask / backdrop | backdrop |
Dialog positioner | positioner |
Dialog root | root |
Dialog headerTitle / title | title |
Dialog closeButton / close | close |
DataTable tableContainer | tableContainer |
DataTable thead | head |
DataTable tbody | body |
DataTable bodyRow / row | row |
Dropdown trigger | trigger |
Dropdown option | option |
DatePicker input | segmented input; identity belongs on group |
See Stable component parts for the documented foundation surfaces.
Migrate a deeply customized product
Section titled “Migrate a deeply customized product”Keep the product’s own tokens, Tailwind utilities, dark/high-contrast selectors, and accessibility preferences. Remove the renderer preset that translated those values into a third-party token system, then map the product values directly onto --cratis-*.
For a custom dialog layer, change renderer part types to Cratis types and rename slots:
import type { DialogParts } from '@cratis/components/Dialogs';import type { StepperParts } from '@cratis/components/CommandDialog';
export const productDialogParts: DialogParts = { backdrop: { className: 'product-dialog-backdrop' }, root: { className: 'product-dialog' }, title: { className: 'product-dialog-title' }, close: { className: 'product-dialog-close' }, content: { className: 'product-dialog-content' }, footer: { className: 'product-dialog-footer' },};
export const productStepperParts: StepperParts = { root: { className: 'product-stepper' }, list: { className: 'product-stepper-list' }, step: { className: 'product-stepper-step' }, header: { className: 'product-stepper-header' }, number: { className: 'product-stepper-number' }, title: { className: 'product-stepper-title' }, panels: { className: 'product-stepper-panels' }, panel: { className: 'product-stepper-panel' },};For an existing nested Prime stepper preset, map the slots by rendered responsibility:
| Components 3 Prime slot | Components 4 part |
|---|---|
nav | list |
panelContainer | panels |
stepperpanel.root | step (<li>) |
stepperpanel.action | header (<button>) |
stepperpanel.number | number |
stepperpanel.title | title |
stepperpanel.content | panel (<section>) |
The old stepperpanel.header wrapper has no one-to-one element. Put list-item layout on step, and interactive-header styling on header. Replace data-p-active selectors with [data-cratis-part='step'][data-active='true'].
Product-owned preset migration
Section titled “Product-owned preset migration”Consider a product that previously supplied styledMode({ preset: ProductPreset }), Prime locale types, a PrimeUI license, and a legacy token bridge:
- Remove
styledMode,ProductPreset, and the license from the Components provider. If the product still renders Prime directly, keep its Prime provider, preset, dependencies, and license beside Components until those direct imports are removed. - Replace
LocaleProps['locales']with product-owned message input and map only Components labels intoCratisComponentsMessages. - Keep
--product-*as the canonical design tokens. Replace the--p-*and--surface-*bridge with direct--cratis-*assignments. - Keep the product’s theme selector and map both light and dark values there; Components does not own the product’s theme lifecycle.
import type { ReactNode } from 'react';import { CratisComponentsProvider, type CratisComponentsMessages,} from '@cratis/components';
interface ProductComponentsProviderProps { children: ReactNode; locale?: string; messages?: CratisComponentsMessages;}
export const ProductComponentsProvider = ({ children, locale = 'en-US', messages,}: ProductComponentsProviderProps) => ( <CratisComponentsProvider value={{ locale, messages }} toaster> {children} </CratisComponentsProvider>);Import Components structure but omit its optional theme, then load the product mapping:
import '@cratis/components/tokens';import '@cratis/components/styles';import './product-components.css';:root { --cratis-surface-0: var(--product-surface); --cratis-surface-100: var(--product-subtle); --cratis-surface-ground: var(--product-canvas); --cratis-surface-section: var(--product-subtle); --cratis-surface-card: var(--product-surface); --cratis-surface-overlay: var(--product-surface); --cratis-surface-hover: var(--product-subtle); --cratis-surface-border: var(--product-border-default); --cratis-control-background: var(--product-surface); --cratis-control-border: var(--product-border-default);
--cratis-text-color: var(--product-text-primary); --cratis-text-color-secondary: var(--product-text-secondary);
--cratis-primary-color: var(--product-accent-700); --cratis-primary-color-text: var(--product-text-inverse); --cratis-primary-300: var(--product-accent-300); --cratis-primary-400: var(--product-accent-400); --cratis-primary-500: var(--product-accent-500); --cratis-primary-600: var(--product-accent-600); --cratis-action-background: var(--product-accent-500); --cratis-action-background-hover: var(--product-accent-600); --cratis-action-background-active: var(--product-accent-700); --cratis-action-text: var(--product-text-inverse);
--cratis-highlight-bg: var(--product-accent-50); --cratis-highlight-text-color: var(--product-accent-700); --cratis-green-500: var(--product-success-fg); --cratis-orange-500: var(--product-warning-fg); --cratis-red-500: var(--product-error-fg); --cratis-info-background: var(--product-info-fg); --cratis-info-text: var(--product-text-inverse); --cratis-success-background: var(--product-success-fg); --cratis-success-text: var(--product-text-inverse); --cratis-warning-background: var(--product-warning-fg); --cratis-warning-text: var(--product-text-inverse); --cratis-danger-background: var(--product-error-fg); --cratis-danger-text: var(--product-text-inverse); --cratis-control-height: var(--product-control-min-size); --cratis-control-height-small: var(--product-control-min-size); --cratis-control-height-large: var(--product-control-min-size); --cratis-border-radius: 6px; --cratis-focus-ring: var(--product-ring-focus); --cratis-maskbg: var(--product-scrim);}Because these assignments reference product tokens, existing dark, enhanced-contrast, control-size, status, and accessibility selectors flow through without duplicating the mapping. The product continues to own typography, spacing, motion, elevation, and component-specific treatments.
If one product area still uses Prime’s locale-aware InputNumber, keep it as an explicitly bounded Prime island. Mount the Prime provider independently around that remaining surface and retain its installed-version theme/license requirements; do not put renderer keys back into CratisComponentsProvider:
import { PrimeReactProvider } from '@primereact/core';
<CratisComponentsProvider value={{ locale, messages }}> <PrimeReactProvider license={primeUiLicense}> <LocaleAwareNumberInput /> </PrimeReactProvider></CratisComponentsProvider>;PrimeReact 11 receives license directly as a provider prop; it does not use the value={{ license }} shape of CratisComponentsProvider. Add the installed Prime theme/provider options beside license when that island needs them.
The provider boundary scopes Prime runtime configuration and context, but a JavaScript-imported Prime theme stylesheet is still a document-global side effect. Put the import in the smallest host entry point that contains the island and inventory any .p-* selectors that intentionally depend on it; wrapping a subtree does not isolate that CSS. Every retained island should have an owner, a reason it remains, its licensing/theme dependencies, and an explicit removal condition or tracking issue.
Other areas can remove Prime as soon as they have no direct Prime imports. Remove the separate Prime provider only when number grouping, decimal handling, fraction digits, prefix/suffix, min/max, and command binding have an accepted renderer-independent replacement.
This preserves product token and theme ownership while removing the circular product → Prime preset → Prime variables → Cratis translation.
Custom filters must migrate in the same change: replace the Prime FilterMatchMode import with DataTableFilterMatchMode, replace registerMatcher with registerDataTableFilterMatcher, and store the returned matchMode in the corresponding constraint. Built-in mode strings remain behaviorally compatible, but using the Cratis constants removes the renderer type dependency; custom registration never crosses registries automatically. Tests and application-owned adapters that must verify the live registered predicate can call resolveDataTableFilterMatcher(matchMode) from the same DataTables subpath.
Migrate directly from Components 2
Section titled “Migrate directly from Components 2”A PrimeReact 10 application does not need to adopt Components 3/PrimeReact 11 before moving to Components 4. Migrate the two boundaries independently:
- Keep the existing Prime 10 provider and Lara/product theme while direct Prime controls remain.
- Mount
CratisComponentsProviderseparately and importtokensplusstyles. - Map the product palette directly to
--cratis-*; omit the baselinethemewhen product CSS owns the appearance. - Use the Cratis
Columnmarker insideDataPage. Alias and retain PrimeColumnonly for grouped/expandable direct Prime tables. - Replace low-risk direct Button, Tag, Badge, Avatar, Message, Progress, Dropdown, Dialog, and Toast surfaces in batches.
- Retain Prime or build product primitives for tabs, sidebars, timelines, select-button groups, menubars, and advanced tables until their requirements have an intentional replacement.
- Retain PrimeIcons while class strings remain; move to React icons/product SVGs separately.
This avoids an unnecessary intermediate Prime 11 migration and does not imply that Components configures the remaining Prime 10 surfaces.
Baseline-first coexistence with PrimeReact 11
Section titled “Baseline-first coexistence with PrimeReact 11”A Components 3 / PrimeReact 11 application can adopt Components 4 while retaining Prime 11 directly:
- Mount Components and Prime providers independently. Keep Prime dependencies, theme, and license for direct Prime controls.
- Remove
styledMode()and@cratis/components/primereact-v10-palettefrom the Components side. - Start with
tokens,styles, andthemepluscratis-darkfor the maintained baseline dark appearance. - Keep temporary legacy
--surface-*aliases in product-owned CSS while direct Prime and old product styles remain; migrate those references to--cratis-*over time. - Replace simple product-owned Prime wrappers where Components or native composition has parity.
- Keep the application-owned grouped/lazy table adapter until #109 or another proven state seam covers its grouping and controlled server sorting.
- Use Components
Toolbaronly for canvas/tool-palette interactions. Keep a native action row for ordinary page actions rather than forcing a canvas toolbar replacement.
Every host entry point that renders Components must import the structural stylesheet. A package that imports Components must also declare it rather than relying on another workspace’s dependency.
Product compositor migration
Section titled “Product compositor migration”A deeply customized product can retain its shaders and measurement wrappers while replacing renderer types and selectors at the Components boundary:
- Type dialog maps as
DialogParts:mask→backdrop,headerTitle→title, andcloseButton→close. - Type stepper maps as
StepperParts:nav→list,panelContainer→panels,stepperpanel.root→step,action→header, andcontent→panel. - Toolbar composition exposes
ToolbarParts,ToolbarButtonParts,ToolbarGroupParts,ToolbarSeparatorParts,ToolbarLayoutParts,ToolbarSectionParts,ToolbarFolderParts, andToolbarFanOutParts. A product measurement wrapper should identify boundaries throughtoolbar-group,toolbar-separator,toolbar-layout,toolbar-section,toolbar-context, andtoolbar-slot*data-cratis-partvalues. Direction, mode, expanded, settled, active, and transitioning state are data attributes, so the product can keep its composited sibling and measurement algorithm without depending on.toolbar*implementation classes. - For integrated Canvas controls, pass the product surface through
controlsGlassSurface, localized actions throughcontrolsLabels, and compositor marker names throughcaptureAttributes. SetdisableControlsGlassonly when the product intentionally wants the low-cost CSS fallback. Components does not hardcode or duplicate product marker vocabulary. - Preserve product-owned capture attributes through the documented Canvas prop and ordinary part attributes; keep capture/compositor implementation in the product.
import { ProductCompositorSurface } from './ProductCompositorSurface';
<Canvas captureAttributes={{ layer: 'data-product-compositor-layer', content: 'data-product-compositor-content', transformHost: 'data-product-compositor-transform-host', }} controlsGlassSurface={<ProductCompositorSurface cornerRadius={999} />} controlsLabels={canvasControlLabels}/>;For direct Prime tables, map value to data. Replace size='small', stripedRows, and Column align with product classes through DataTableParts / Column body and header classes. Stop and keep an application-owned or Prime table when the surface requires grouping, row expansion, or controlled lazy/server sorting that DataTableCore does not claim to provide. Move Prime Column imports used inside DataPage to the Cratis marker independently from those advanced tables.
Removing Prime UI imports is not the same as removing a deliberate Prime schema/prototype catalog. If a product keeps Prime metadata generation or a PrimeReact prototype workspace, Prime remains an intentional tooling/product dependency and must be versioned and licensed on that basis even after application screens migrate.
Paginator callbacks that formerly returned classes from renderer context must become static Cratis parts plus CSS state selectors:
import type { TablePaginatorParts } from '@cratis/components/DataTables';
export const productPaginatorParts: TablePaginatorParts = { root: { className: 'product-paginator' }, first: { root: { className: 'product-paginator-button' } }, previous: { root: { className: 'product-paginator-button' } }, next: { root: { className: 'product-paginator-button' } }, last: { root: { className: 'product-paginator-button' } }, info: { className: 'product-paginator-info' },};Pass the parts to either query-backed table:
<DataTableForQuery query={AllProducts} paginatorPt={productPaginatorParts} emptyMessage='No products'> <Column field='name' header='Name' /></DataTableForQuery>DataTableForObservableQuery uses the same paginatorPt prop. Use :disabled, :focus-visible, and the documented data-cratis-* states in CSS instead of renderer callback context. The numbered-page renderer is gone; the paginator reports the current page and provides first/previous/next/last actions.
Dropdown.inputId and Dropdown.panelClassName remain migration aliases for id and pt.popover.className, but new code should use the current names.
Update DatePicker integration
Section titled “Update DatePicker integration”DatePickerInput still accepts and emits Date | null, but its internal value uses @internationalized/date. Formatting now follows the active locale and calendar rather than a PrimeReact mask.
- Replace
dateFormatwith locale configuration where possible. The prop remains accepted but is ignored. - Use
aria-labeloraria-labelledbyfor the segmented date group. ididentifies the focus group rather than a native text input.- The accessible calendar trigger is shown by default; set
showIcon={false}only for segment-entry-only experiences. todayLabelandclearLabeloverride the provider messages for one picker.showTimeandhourFormatremain in the current API.
Update Dropdown styling and semantics
Section titled “Update Dropdown styling and semantics”Dropdown preserves the value, options, optionLabel, optionValue, filtering, clear, and change-event model. Single selects now follow the WAI-ARIA button/listbox pattern; filtered selects use a combobox.
Do not assume every Dropdown trigger has role="combobox". Query it by its accessible name or data-cratis-part="trigger" in tests.
Multiple selection uses a native multiple-select when filtering is off and an accessible multi-value combobox when filter is enabled. Prefer a dedicated collection picker for a large or highly customized multi-select experience.
Update Tooltip triggers
Section titled “Update Tooltip triggers”Tooltip now enhances the actual trigger so focus, hover, and aria-describedby stay on one element. Its children contract is one React element rather than an arbitrary node list. Wrap text, fragments, multiple siblings, or conditional content in one appropriate native control before passing it to Tooltip. className is merged onto that trigger instead of an extra wrapper.
import { FaGear } from 'react-icons/fa6';import { Tooltip } from '@cratis/components/Common';
<Tooltip content='Account settings'> <button type='button' aria-label='Account settings'> <FaGear aria-hidden='true' /> </button></Tooltip>;Update tables
Section titled “Update tables”DataTableCore now renders semantic HTML. Query-backed paging remains owned by Arc.
- Sorting and filtering apply to the currently loaded page.
- Complete-result filtering and sorting are not automatic table state. Model them in query arguments and implement them in the server query before paging.
clientFilteringremains temporarily accepted as a deprecated no-op so staged source migrations compile. Remove it: filtering is always scoped to the loaded page, and complete-result filtering belongs on the server before paging.- Legacy
{ operator, constraints }filter entries remain accepted.operator: 'or'matches any constraint; all other values match every constraint. Columnremains the declarative column marker. Its selection-column contract is explicitly single-row (selectionMode='single'); the old'multiple'type advertised checkbox behavior that the implementation never provided. Build multiple selection as an explicit product interaction rather than relying on that removed value.- Table styling uses
DataTablePartsanddata-cratis-part. - Server totals remain authoritative for the paginator.
Prime’s built-in match-mode string values continue to work because Components implements the same common predicates directly. Replace the renderer constants with Cratis constants to remove the type dependency:
| Prime constant | Cratis constant |
|---|---|
FilterMatchMode.STARTS_WITH | DataTableFilterMatchMode.StartsWith |
FilterMatchMode.CONTAINS | DataTableFilterMatchMode.Contains |
FilterMatchMode.EQUALS | DataTableFilterMatchMode.Equals |
FilterMatchMode.IN | DataTableFilterMatchMode.In |
FilterMatchMode.DATE_BEFORE | DataTableFilterMatchMode.DateBefore |
FilterMatchMode.DATE_AFTER | DataTableFilterMatchMode.DateAfter |
Custom matcher registration is different: Prime FilterService.register() or an application helper around it does not populate the Components registry. Replace the registration and the constraint together:
import { DataTableFilterMatchMode, registerDataTableFilterMatcher, type DataTableFilterMeta,} from '@cratis/components/DataTables';
const roleMatcher = registerDataTableFilterMatcher( 'product.roleContains', (value, filter) => String(value ?? '') .toLocaleLowerCase() .includes(String(filter ?? '').toLocaleLowerCase()),);
const filters: DataTableFilterMeta = { name: { value: 'Sample User', matchMode: DataTableFilterMatchMode.Contains }, role: { value: 'admin', matchMode: roleMatcher.matchMode },};
// Call roleMatcher.unregister() when the owning integration is permanently removed.A retained arbitrary match-mode string keeps source compatibility but does not register behavior. Unknown modes deliberately match nothing rather than silently applying the wrong predicate.
Separate RadioButtonField options bound to one property now require the same explicit name prop so native arrow-key radio-group navigation works. RadioGroupField and RatingField generate a shared internal name automatically.
Update dialogs and steppers
Section titled “Update dialogs and steppers”Dialog callback, busy, validity, dismissal, and initial-focus contracts remain. The modal/focus implementation is now React Aria-based.
Stepper parts are Cratis-owned: root, list, step, header, number, title, separator, panels, and panel. Custom CSS that targeted Prime stepper classes or roles must move to those parts.
Update notifications
Section titled “Update notifications”The imperative API remains:
import { toast } from '@cratis/components/Notifications';
toast.success({ title: 'Saved', description: 'Your changes were saved.',});The queue, promise lifecycle, dispatch substitution, timeout pause, focus behavior, frames, and region are Cratis-owned. Toast part keys are region, toast, icon, content, title, description, action, and close.
Replace direct Prime imports
Section titled “Replace direct Prime imports”Components cannot remove PrimeUI licensing from an application that still imports Prime directly. Replace those imports with Components, native HTML, or application-owned primitives.
Typical replacements:
| Prime import | Preferred replacement |
|---|---|
primereact/button | Button from @cratis/components/Common |
primereact/inputtext | CommandForm field or native styled input |
primereact/dialog | Dialog from @cratis/components/Dialogs |
primereact/dropdown / select | Dropdown |
primereact/datatable / column | DataTableCore / Column |
primereact/tag, badge, message | @cratis/components/Display |
primereact/toast / toaster | @cratis/components/Notifications |
Complete PrimeIcons class strings remain usable where a component accepts the public Icon type, but Components no longer installs the font or adds a missing provider base class. A consumer that intentionally retains PrimeIcons must load its stylesheet and pass the complete class string. Prefer a React icon component or product-owned SVG. DataPage.MenuItem.icon remains a React component type rather than the shared Icon union.
Verify the migration
Section titled “Verify the migration”- Remove unused Prime dependencies and the PrimeUI license/provider configuration.
- Import
tokensandstyles; choose the baselinethemeor map product tokens. - Replace global Prime presets with Cratis tokens.
- Update
ptkeys and CSS selectors to Cratis parts. - Replace direct Prime imports.
- Exercise dialogs, filtered tables, dates, dropdowns, toasts, and steppers with keyboard-only navigation.
- Verify light, dark, forced-colors, reduced-motion, and responsive layouts.
- Run TypeScript, specs, Storybook, and the production build.
- Import components from their explicit subpath rather than the removed root namespace; apply the mapping table under Import from explicit subpaths, or run the migration codemod.
A TypeScript 6 application using skipLibCheck: false may see bounded upstream diagnostics from Pixi’s @webgpu/types collision with TypeScript’s built-in WebGPU declarations, from @cratis/arc.react’s published global JSX declarations, or under NodeNext from extensionless declaration imports in the current Arc and Fundamentals packages. Components validates every packed subpath without suppressing these diagnostics; exact versions, codes, affected subpaths, and removal conditions are documented under Strict public-type validation and tracked in #176.
For renderer ownership, coexistence, custom composition, unsupported claims, and licensing, read Renderer adapters and coexistence. For the decision, trade-offs, and validation gates, read UI foundation. For the older 2.x → 3.x PrimeReact migration, see Migrate from Components 2 to 3.