Skip to content

CratisComponentsProvider

CratisComponentsProvider is the application root for Components-owned locale/messages and optional renderer selection. Its value remains renderer-independent and it does not configure an application’s direct third-party component usage; a selected adapter may mount its own provider boundary. Styling remains owned by CSS or by the explicitly selected adapter.

import { CratisComponentsProvider } from '@cratis/components';
export const App = () => (
<CratisComponentsProvider value={{ locale: 'en-US' }} toaster>
<Application />
</CratisComponentsProvider>
);

value and toaster are optional; children is the only required prop. With no value, the provider uses en-US and the English labels in cratisDefaults. An invalid locale string also falls back to en-US.

The package root exports only the provider and its configuration helpers. Import every component from its subpath, such as @cratis/components/Common or @cratis/components/Toolbar.

CratisComponentsProvider does not replace Arc’s provider. Command and query components, such as CommandDialog, DataPage and ChatSidebarForObservableQueries, get their origin, API base path, microservice, and shared query cache from <Arc> in @cratis/arc.react. Render <Arc> around the Components provider:

import { Arc } from '@cratis/arc.react';
import { CratisComponentsProvider } from '@cratis/components';
export const App = () => (
<Arc>
<CratisComponentsProvider value={{ locale: 'en-US' }} toaster>
<Application />
</CratisComponentsProvider>
</Arc>
);

Pass Arc’s own props, such as microservice or apiBasePath, to <Arc>. The Components provider reads no Arc configuration. An application that uses no Arc-bound components can leave <Arc> out.

The provider memoizes its resolved configuration by the value object identity. For an application root that re-renders frequently, keep a configured object stable rather than constructing a large message catalog inline on every render:

const componentsConfig = {
locale: 'en-US',
messages: productMessages,
};
<CratisComponentsProvider value={componentsConfig} toaster>
<Application />
</CratisComponentsProvider>;

A small inline object as in the basic example is inexpensive; the stable form matters when the value contains a larger product-owned message catalog.

MemberPurpose
localeBCP 47 locale used by React Aria for dates, numbers, keyboard behavior, and announcements.
messages.paginatorComponents-owned paginator labels, including the optional range(start, end, total) builder for the visible range text.
messages.datePickerComponents-owned date-picker action/navigation labels, plus the segmented input’s fallback accessible name (label).
messages.dropdownDropdown’s show-options and clear-selection labels.
messages.dialogAction/dismissal labels (ok, cancel, yes, no, close, refused) shared by Dialog, CommandDialog, and StepperCommandDialog.
messages.stepperNavigation labels (next, previous, submit) shared by CommandStepper and StepperCommandDialog.
messages.notificationsToaster’s dismiss-action and region-landmark labels.
messages.dataTableDataTableCore’s search, selection, and loading (loading), failed (failed), and access-denied (unauthorized) messages, and the empty result of an active search (filteredEmpty for the loaded page, filteredEmptyServer in server mode).
messages.filterFilterPanel dialog and PivotViewer filter button name (label), and panel/group/standalone CheckboxListFilter search name (searchAriaLabel).
messages.columnFilterThe built-in column filter popup’s clear/apply/boolean/match-mode labels.
messages.toolbarToolbar’s accessible-name fallback (label, default Tools) and the drawer folder’s close button (closeDrawer, default Close).
iconsComponents-owned icon vocabulary replacing the built-in glyphs the library draws. See Register an icon set.
localesTemporary Components 3 compatibility map; migrate to messages.

Unknown Components 3 renderer options are intentionally a type error. Remove license, theme, defaults, global pt, ptOptions, ripple, unstyled, and renderer z-index settings rather than compiling a provider whose visual configuration does nothing. Configure any remaining direct Prime provider independently.

The certified MUI, PrimeReact 11, and PrimeReact 10 manifests implement the stable nine-slot stable-presentation/v1 profile. That stable profile adapts only Button, IconButton, TextInput, TextArea, Checkbox, Radio, Switch, ProgressBar, and Surface. It never means full-catalog replacement.

Pass one stable presentation manifest to library. The broader fourteen-slot renderer system, ordered manifest composition, scopes/islands, atomic slots, lazy loading, and public adapter discovery remain experimental even though some unstable_ contracts are available to repository and adapter authors.

The provider exposes:

PropPurpose
libraryOne stable presentation manifest. Ordered arrays are experimental last-wins composition. Omit for built-in.
libraryModeExperimental profile-promise behavior: strict rejects invalid promises; degrade reports after mount.
rendererFallbackcore keeps the built-in slot fallback; throw rejects an undeclared slot instead of expanding adapter scope.
overlayEnvironmentStable, post-commit portal-container lookup through CratisOverlayEnvironment.
rendererSetupStable adapter-declared boolean attestations. Never put credentials, keys, or caches here.

Adapter packages declaration-merge their own keys into CratisRendererSetupExtensions, so importing an adapter gives typed setup without adding vendor fields to Core. The provider copies and freezes boolean entries, discards non-boolean runtime values, inherits the map through nested providers unless a nested map replaces it wholesale, and forwards it to the selected library provider.

A key-gated renderer still receives its key through the application’s own outer vendor provider. For example, the PrimeReact 11 adapter receives only a boolean assertion that the application completed that setup; Components never receives the key itself. See the adapter package README for the exact provider and build-environment wiring, and the renderer licensing policy for the cross-adapter ownership rule.

Every message group follows the same precedence: a named component prop (or its own labels/filterLabels override) → provider message → English default. Per-instance overrides keep working. For filter search names, the effective placeholder comes between the provider message and 'Search', preserving the placeholder fallback when no provider search name is set. PivotViewer supplies its own panel search name from labels.search (default Search…), so messages.filter.searchAriaLabel does not name that search. Its group searches use labels.searchGroup or Search ${groupLabel} rather than assigning one provider string to every group.

Message keyDefaultUsed when
messages.filter.labelFiltersNo explicit panel aria-label (or PivotViewer labels.filters) is passed; it also names PivotViewer’s filter button.
messages.filter.searchAriaLabelUnset; effective placeholder, then SearchNo explicit panel, group, or standalone CheckboxListFilter searchAriaLabel is passed. PivotViewer supplies its own panel and group names.

Filter messages do not change visible placeholders. A partial filter group leaves the other key at its default. As in sibling groups, an empty string is a supplied value: label: '' leaves the dialog name empty; searchAriaLabel: '' (or an explicit empty search name) falls through the search input’s existing nonempty-name guard to the English 'Search', not to its placeholder.

English, spelled out explicitly (this is also what every group defaults to with no provider at all):

<CratisComponentsProvider
value={{
locale: 'en-US',
messages: {
paginator: {
navigation: 'Pagination',
first: 'First page',
previous: 'Previous page',
next: 'Next page',
last: 'Last page',
},
datePicker: {
today: 'Today',
clear: 'Clear',
openCalendar: 'Open calendar',
previousMonth: 'Previous month',
nextMonth: 'Next month',
label: 'Date',
},
dropdown: {
showOptions: 'Show options',
clearSelection: 'Clear selection',
},
dialog: {
ok: 'Ok',
cancel: 'Cancel',
yes: 'Yes',
no: 'No',
close: 'Close',
},
stepper: {
next: 'Next',
previous: 'Previous',
submit: 'Submit',
},
notifications: {
dismiss: 'Dismiss',
region: 'Notifications',
},
dataTable: {
selectRow: 'Select row',
selectAllRows: 'Select all rows',
search: 'Search…',
searchAriaLabel: 'Search table',
loading: 'Loading…',
failed: 'Could not load data.',
unauthorized: 'You are not authorized to view this data.',
filteredEmpty: 'No rows on this page match the search.',
filteredEmptyServer: 'No rows match the search.',
},
filter: {
label: 'Filters',
// searchAriaLabel is omitted by default so placeholders name searches.
},
columnFilter: {
matchModeAriaLabel: 'Match mode',
clear: 'Clear',
apply: 'Apply',
true: 'True',
false: 'False',
filterTriggerAriaLabel: (field) => `Filter by ${field}`,
valueAriaLabel: (field) => `Filter value for ${field}`,
},
toolbar: {
label: 'Tools',
closeDrawer: 'Close',
},
},
}}
>
<Application />
</CratisComponentsProvider>

The same shape in Norwegian Bokmål:

<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',
label: 'Dato',
},
dropdown: {
showOptions: 'Vis alternativer',
clearSelection: 'Fjern valg',
},
dialog: {
ok: 'Ok',
cancel: 'Avbryt',
yes: 'Ja',
no: 'Nei',
close: 'Lukk',
},
stepper: {
next: 'Neste',
previous: 'Forrige',
submit: 'Send inn',
},
notifications: {
dismiss: 'Lukk',
region: 'Varsler',
},
dataTable: {
selectRow: 'Velg rad',
selectAllRows: 'Velg alle rader',
search: 'Søk…',
searchAriaLabel: 'Søk i tabellen',
loading: 'Laster…',
failed: 'Kunne ikke laste data.',
unauthorized: 'Du har ikke tilgang til disse dataene.',
filteredEmpty: 'Ingen rader på denne siden samsvarer med søket.',
filteredEmptyServer: 'Ingen rader samsvarer med søket.',
},
filter: {
label: 'Filtre',
searchAriaLabel: 'Søk i filtre',
},
columnFilter: {
matchModeAriaLabel: 'Sammenligningsmodus',
clear: 'Tøm',
apply: 'Bruk',
true: 'Sann',
false: 'Usann',
filterTriggerAriaLabel: (field) => `Filtrer på ${field}`,
valueAriaLabel: (field) => `Filterverdi for ${field}`,
},
toolbar: {
label: 'Verktøy',
closeDrawer: 'Lukk',
},
},
}}
>
<Application />
</CratisComponentsProvider>

React Aria supplies locale data for its interaction patterns — calendar month/weekday names, number formatting, and similar platform locale data. Components asks you only for the product labels it owns; do not copy React Aria’s own locale strings into messages.

Components draws its own glyphs — a × to dismiss, a ⌄ to expand, ▲/▼ for sort direction. A product with its own iconography registers icons once at the root and every component that draws that concept follows, without passing a prop at each call site:

import { CratisComponentsProvider } from '@cratis/components';
import { CloseIcon, ChevronDownIcon } from './icons'; // your product's icon components
const componentsConfig = {
locale: 'en-US',
icons: {
close: <CloseIcon />,
remove: <CloseIcon />,
expand: <ChevronDownIcon />,
},
};
<CratisComponentsProvider value={componentsConfig}>
<Application />
</CratisComponentsProvider>;

The map is partial by design: an unregistered name keeps its built-in glyph, so the example above re-icons dismissal, removal and expansion and leaves sorting and pagination alone. Registering nothing at all renders exactly what Components has always rendered — the built-in glyph lives at its call site and is deliberately not part of cratisDefaults.

NameDrawsDefaultUsed by
closeDismiss this surface×Dialog header close, Toaster toast dismiss, ChatSidebar, the Chat panel, PivotViewer’s detail panel
removeRemove this value×TagGroup tag remove, Chip remove control, CommandForm’s chips field
expandOpen a closed surface⌄ (▾ in ComboBox)Dropdown trigger (all three render branches), ComboBox trigger
sortAscendingAscending sort indicator▲DataTableCore sortable column header
sortDescendingDescending sort indicator▼DataTableCore sortable column header
previousStep backwards‹TablePaginator previous page, DatePickerInput previous month
nextStep forwards›TablePaginator next page, DatePickerInput next month
clearClear the current value×Dropdown clear-selection (all four render branches), the Filter subpath’s FilterPanel clear (also used by PivotViewer)

Every name is a concept more than one component draws. Names are public contract and hard to withdraw, so the vocabulary starts here and grows as products ask for a concept by name rather than covering every glyph in the library at once. Icons a call site supplies — Button’s icon, Toolbar items, Message’s and a toast’s own icon — are yours already and are not part of it.

Icons resolve per site as per-component prop → provider icon → built-in glyph, the same order messages uses for strings. A prop such as Dialog.closeIcon or TagGroup.removeIcon names one call site — “this one dialog”, “this one tag group” — so it keeps winning; the provider speaks for the product and replaces the built-in default everywhere else. Both props keep working unchanged.

import { Dialog } from '@cratis/components/Dialogs';
<CratisComponentsProvider value={{ icons: { close: <CloseIcon /> } }}>
{/* draws <CloseIcon /> */}
<Dialog title='Product-wide'>
<p>Uses the registered close icon.</p>
</Dialog>
{/* draws <Back /> */}
<Dialog title='This one' closeIcon={<Back />}>
<p>Uses its own close icon.</p>
</Dialog>
</CratisComponentsProvider>

A name registered as null draws no glyph while the button, its stable part and its accessible name stay — the same meaning closeIcon={null} already has. Icons never touch an element’s part, className, aria-label or keyboard behavior; only the mark inside changes.

icons merges by name. Like messages, a partial map leaves everything it omits alone; unlike messages, an icon value is an opaque node and is carried over by identity rather than deep-merged into, so registering a React element hands that exact element to the component.

useCratisIcon() returns the same resolver Components uses internally, for a product component that wants to follow the registered vocabulary:

import type { ReactNode } from 'react';
import { useCratisIcon } from '@cratis/components';
interface RemoveButtonProps {
icon?: ReactNode;
onRemove: () => void;
}
export const RemoveButton = ({ icon, onRemove }: RemoveButtonProps) => {
const resolveIcon = useCratisIcon();
return (
<button type='button' aria-label='Remove' onClick={onRemove}>
<span aria-hidden='true'>{resolveIcon('remove', '×', icon)}</span>
</button>
);
};

The vocabulary replaces a glyph, never an element that publishes a part. ProgressSpinner is the example that draws the line: its ring looks like an icon, but it is an svg carrying the svg, track and range parts, and a registered icon that replaced it would silently delete three parts a product may already be styling. Sites like that keep their own markup, and a product restyles them through the parts they publish.

expand has two built-in glyphs today — ⌄ on a Dropdown trigger and ▾ on a ComboBox trigger. Registering expand makes both consistent; registering nothing leaves each as it is, so no existing rendering changes.

Pass toaster to mount the app-wide notification region:

<CratisComponentsProvider
value={{ locale: 'en-US' }}
toaster={{ position: 'top-right', dismissAriaLabel: 'Dismiss notification' }}
>
<Application />
</CratisComponentsProvider>

toaster={true} mounts the toaster with its defaults; an object is passed to it as ToasterProps (position, limit, timeout, dismissAriaLabel, regionAriaLabel, pt). You may instead mount <Toaster /> from @cratis/components/Notifications yourself when its placement belongs elsewhere in the application tree. Mount only one of the two: every toaster renders the same global toast queue, so two toasters show each toast twice.

The stable overlayEnvironment contract lets an application choose where Components-owned portals mount without consulting browser globals during import or server rendering:

import {
CratisComponentsProvider,
type CratisComponentsProviderProps,
} from '@cratis/components';
const overlayEnvironment: NonNullable<
CratisComponentsProviderProps['overlayEnvironment']
> = {
getContainer: () =>
typeof document === 'undefined'
? null
: document.getElementById('application-overlays'),
};
export const App = () => (
<CratisComponentsProvider
value={{ locale: 'en-US' }}
overlayEnvironment={overlayEnvironment}
>
<Application />
<div id='application-overlays' />
</CratisComponentsProvider>
);

Returning null defers the overlay; Components does not silently retarget it to document.body. The non-modal ChatSidebar and FilterPanel also use this container, or document.body when no environment is configured, and portal only after mount. An open FilterPanel inside a Cratis modal Dialog mounts within that modal instead when the configured container is outside it, preserving focus and accessibility; a container inside the modal is used as configured. Direct vendor overlays keep their own portal and z-index configuration. Verify layer order and focus behavior in the real application shell when both systems can open together.

Import tokens and styles, then choose the baseline theme or your own token values. The provider has no styling responsibility.

import '@cratis/components/tokens';
import '@cratis/components/styles';
import '@cratis/components/theme';

For custom products, see Cratis tokens and Stable component parts.

ExportPurpose
CratisComponentsConfigRenderer-independent provider configuration.
CratisComponentsMessagesComponents-owned message groups.
CratisComponentsIconsComponents-owned icon vocabulary.
useCratisIcon()Resolver for one icon site: prop, then provider, then built-in glyph.
CratisIconResolverType of the resolver useCratisIcon() returns.
cratisDefaultsDefault locale and English labels.
mergeCratisComponentsConfig()Pure configuration merge helper.
useCratisComponentsConfig()Reads the resolved configuration inside a component.