---
title: Migrate from Components 3 to 4
description: Move from the PrimeReact-backed release to the renderer-independent React Aria foundation.
---


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.

:::note
An application that has not migrated remains on its Components 3 package profile, including the third-party dependencies declared by that version. Components `>=3 <4` is in maintenance/security-critical support while Components `>=4 <5` is the current candidate and migration target. Owners must decide and approve the Components 3 EOL date no later than 12 months after Components 4 GA. Components 4 does not provide a transparent Prime compatibility package. Its repository-versioned `@cratis/components.mui`, `@cratis/components.primereact` (PrimeReact 11), and `@cratis/components.primereact10` adapters are optional and cover only the nine `common.*` presentation slots.
:::

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

1. 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.
2. While the installed Core package is still in the Components 3 source window (`>=3 <4`), preview and apply `cratis-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.
3. 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.
4. 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.
5. 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

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.

```bash
# npm
npm uninstall primereact primeicons @primereact/core @primereact/headless \
  @primereact/hooks @primereact/styles @primereact/types @primeuix/themes
npm install '@cratis/components@^4.0.0'

# pnpm
pnpm remove primereact primeicons @primereact/core @primereact/headless \
  @primereact/hooks @primereact/styles @primereact/types @primeuix/themes
pnpm add '@cratis/components@^4.0.0'

# Yarn
yarn remove primereact primeicons @primereact/core \
  @primereact/headless @primereact/hooks @primereact/styles @primereact/types \
  @primeuix/themes
yarn 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

The canonical rule going forward: **the package root is setup-only; every component ships from its own subpath.**

```tsx
// 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>;
```

```tsx
// After — canonical subpath, named imports
import { 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:

```bash
TOOLING_RANGE='^4.0.0'
npx --package "@cratis/components.migrator@$TOOLING_RANGE" \
  cratis-components-remove-root-namespace-imports --check path/to/app/src
npx --package "@cratis/components.migrator@$TOOLING_RANGE" \
  cratis-components-remove-root-namespace-imports path/to/app/src
```

Components 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:

```bash
# pnpm
pnpm 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/src
```

Quote 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

Run the root-import codemod above first. The Button and callback codemods resolve Components-owned identifiers from explicit subpaths, so the authoritative order is:

1. `cratis-components-remove-root-namespace-imports`
2. `cratis-components-button-variant-tone`
3. `cratis-components-change-handler`

Use the bounded Components 4 tooling train and preview each transform before applying it:

```bash
TOOLING_RANGE='^4.0.0'

npx --package "@cratis/components.migrator@$TOOLING_RANGE" \
  cratis-components-button-variant-tone --check path/to/app/src
npx --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/src
npx --package "@cratis/components.migrator@$TOOLING_RANGE" \
  cratis-components-change-handler path/to/app/src
```

Keep 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:

```tsx
// 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:

```tsx
// 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:

```tsx
// 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:

```bash
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](/components/ui-foundation/#capability-profiles) for how these subpaths group into Foundation, Advanced React, and Spatial, and the [capability matrix](/components/ui-foundation/#capability-matrix) for what each profile owns.

## Keep the stylesheet entry points

The three Cratis-owned stylesheet entries remain:

```ts
import '@cratis/components/tokens';
import '@cratis/components/styles';
import '@cratis/components/theme'; // optional baseline appearance
```

- `tokens` defines the stable semantic `--cratis-*` variables.
- `styles` contains structural rules for every component.
- `theme` supplies 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

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.

```tsx
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

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

| 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:

```tsx
<CratisComponentsProvider value={{ license, ...styledMode({ preset: ProductPreset }) }}>
    <App />
</CratisComponentsProvider>
```

After:

```tsx
<CratisComponentsProvider value={{ locale: 'en-US' }}>
    <App />
</CratisComponentsProvider>
```

Map product tokens directly in CSS:

```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

An audit of the package `exports` map ([#173](https://github.com/Cratis/Components/issues/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

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.

```tsx
<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

| 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](/components/styling/pass-through/) for the documented foundation surfaces.

### 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:

```ts
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

Consider a product that previously supplied `styledMode({ preset: ProductPreset })`, Prime locale types, a PrimeUI license, and a legacy token bridge:

1. 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.
2. Replace `LocaleProps['locales']` with product-owned message input and map only Components labels into `CratisComponentsMessages`.
3. Keep `--product-*` as the canonical design tokens. Replace the `--p-*` and `--surface-*` bridge with direct `--cratis-*` assignments.
4. Keep the product's theme selector and map both light and dark values there; Components does not own the product's theme lifecycle.

```tsx
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:

```ts
import '@cratis/components/tokens';
import '@cratis/components/styles';
import './product-components.css';
```

```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`:

```tsx
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

A PrimeReact 10 application does not need to adopt Components 3/PrimeReact 11 before moving to Components 4. Migrate the two boundaries independently:

1. Keep the existing Prime 10 provider and Lara/product theme while direct Prime controls remain.
2. Mount `CratisComponentsProvider` separately and import `tokens` plus `styles`.
3. Map the product palette directly to `--cratis-*`; omit the baseline `theme` when product CSS owns the appearance.
4. Use the Cratis `Column` marker inside `DataPage`. Alias and retain Prime `Column` only for grouped/expandable direct Prime tables.
5. Replace low-risk direct Button, Tag, Badge, Avatar, Message, Progress, Dropdown, Dialog, and Toast surfaces in batches.
6. Retain Prime or build product primitives for tabs, sidebars, timelines, select-button groups, menubars, and advanced tables until their requirements have an intentional replacement.
7. 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

A Components 3 / PrimeReact 11 application can adopt Components 4 while retaining Prime 11 directly:

1. Mount Components and Prime providers independently. Keep Prime dependencies, theme, and license for direct Prime controls.
2. Remove `styledMode()` and `@cratis/components/primereact-v10-palette` from the Components side.
3. Start with `tokens`, `styles`, and `theme` plus `cratis-dark` for the maintained baseline dark appearance.
4. Keep temporary legacy `--surface-*` aliases in product-owned CSS while direct Prime and old product styles remain; migrate those references to `--cratis-*` over time.
5. Replace simple product-owned Prime wrappers where Components or native composition has parity.
6. Keep the application-owned grouped/lazy table adapter until #109 or another proven state seam covers its grouping and controlled server sorting.
7. Use Components `Toolbar` only 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

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`, and `closeButton` → `close`.
- Type stepper maps as `StepperParts`: `nav` → `list`, `panelContainer` → `panels`, `stepperpanel.root` → `step`, `action` → `header`, and `content` → `panel`.
- Toolbar composition exposes `ToolbarParts`, `ToolbarButtonParts`, `ToolbarGroupParts`, `ToolbarSeparatorParts`, `ToolbarLayoutParts`, `ToolbarSectionParts`, `ToolbarFolderParts`, and `ToolbarFanOutParts`. A product measurement wrapper should identify boundaries through `toolbar-group`, `toolbar-separator`, `toolbar-layout`, `toolbar-section`, `toolbar-context`, and `toolbar-slot*` `data-cratis-part` values. 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 through `controlsLabels`, and compositor marker names through `captureAttributes`. Set `disableControlsGlass` only 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.

```tsx
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:

```ts
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:

```tsx
<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

`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 `dateFormat` with locale configuration where possible. The prop remains accepted but is ignored.
- Use `aria-label` or `aria-labelledby` for the segmented date group.
- `id` identifies 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.
- `todayLabel` and `clearLabel` override the provider messages for one picker.
- `showTime` and `hourFormat` remain in the current API.

## 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

`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.

```tsx
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

`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.
- `clientFiltering` remains 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.
- `Column` remains 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 `DataTableParts` and `data-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:

```ts
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

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

The imperative API remains:

```ts
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

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

1. Remove unused Prime dependencies and the PrimeUI license/provider configuration.
2. Import `tokens` and `styles`; choose the baseline `theme` or map product tokens.
3. Replace global Prime presets with Cratis tokens.
4. Update `pt` keys and CSS selectors to Cratis parts.
5. Replace direct Prime imports.
6. Exercise dialogs, filtered tables, dates, dropdowns, toasts, and steppers with keyboard-only navigation.
7. Verify light, dark, forced-colors, reduced-motion, and responsive layouts.
8. Run TypeScript, specs, Storybook, and the production build.
9. Import components from their explicit subpath rather than the removed root namespace; apply the mapping table under [Import from explicit subpaths](#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](/components/ui-foundation/#strict-public-type-validation) and tracked in [#176](https://github.com/Cratis/Components/issues/176).

For renderer ownership, coexistence, custom composition, unsupported claims, and licensing, read
[Renderer adapters and coexistence](/components/renderers/). For the decision, trade-offs, and
validation gates, read [UI foundation](/components/ui-foundation/). For the older 2.x → 3.x PrimeReact
migration, see [Migrate from Components 2 to 3](/components/migration/2-to-3/).
