git:20261014.10e84c0 to git:20261014.e378299

1 added, 1 removed. Audit A to A.

---
name: cratis-components-styling
description: Style and theme an application built on Cratis Components 4 — the three stylesheet entries an app must import, the --cratis-* design-token seam, choosing between the baseline theme, a product theme and fully owned styling, dark mode, the typed pt parts and data-cratis-part selectors, the optional renderer adapters, and where Tailwind actually fits. Use when setting up a new Cratis frontend, changing colors or theming, fixing components that render unstyled, or styling one part of a component. Do not use for component API or page composition questions.
license: MIT
---
# Styling Cratis Components
Cratis Components 4 owns its markup and its styling contract. There is no
third-party theme engine, preset or provider underneath it: what a Cratis
application looks like is decided by three Cratis-owned stylesheet entries and
the `--cratis-*` custom properties they read. Getting the setup wrong shows up as
components rendering completely unstyled, which is the single most common
first-run problem — start by checking the CSS entry.
## Verified product sources
| Package | Verified from |
| --- | --- |
| `@cratis/components` | its package manifest (`exports`, no vendor UI dependency or peer), `Source/tokens.css`, `Source/MIGRATION.md`, `Documentation/Styling/*`, `Documentation/Common/cratis-components-provider.md`, `Documentation/renderers/*` |
Components declares **no** PrimeReact, PrimeIcons or PrimeUI dependency or peer.
React Aria supplies focus, overlay, collection and date behavior *internally* —
you never import it, never style its class names, and it never appears in an
application's manifest. Remove any leftover Prime packages that only Components
used to need (`Source/MIGRATION.md` lists them); keep one only if the application
itself still imports it directly, and then that island's provider, theme and
license are the application's.
## Step 1 — What an app must do
Two things, or the components render unstyled:
1. **Import the stylesheets, in this order**, in `index.css`; the entry point
imports that file with `import './index.css'`:
```css
@import '@cratis/components/tokens';
@import '@cratis/components/styles';
@import '@cratis/components/theme'; /* optional baseline */
```
Direct CSS subpath imports from TypeScript need ambient declarations on
TypeScript 6, otherwise TS2882. To avoid the roughly 32 KiB gzip aggregate,
replace `styles` with `styles/base` and the needed `<subpath>/styles` entries
(for example `Dialogs/styles`, `DataTables/styles`). Area order does not matter.
`tokens` first — `styles` and `theme` both read it. `styles` also carries the
split-pane CSS the `DataPage` details pane needs; use the aggregate or the
corresponding area sheets even in a fully custom-styled app. `styles` contains **no** Tailwind Preflight, no
global reset and no copy of the token values.
2. **Mount `CratisComponentsProvider`** (from the package root,
`@cratis/components`) above every Cratis component. It carries `locale` and
the Components-owned `messages`, and mounts the toast region with `toaster`;
it carries **no** theme. `toaster` accepts `boolean | ToasterProps`: without
it or a standalone `<Toaster />`, no toast appears; mount one, never both.
Message groups are `paginator`, `datePicker`, `dropdown`, `dialog`, `stepper`,
`notifications`, `dataTable`, `filter`, `columnFilter`, `toolbar`. `locales`
is deprecated. `value.icons` replaces `close`, `remove`, `expand`,
`sortAscending`, `sortDescending`, `previous`, `next`, `clear`; a component
prop wins over the provider, then the built-in glyph (`null` draws none).
`useCratisIcon()` resolves that vocabulary. Provider props also include
`overlayEnvironment`, `rendererSetup` (non-secret booleans), experimental
`libraryMode` (`strict` / `degrade`) and `rendererFallback` (`core` / `throw`).
The Components 3 renderer keys — `license`, `theme`,
`defaults`, global `pt`, `ptOptions`, `ripple`, `unstyled`, z-index — are a
type error on purpose, so a migrated app cannot compile a provider whose
visual configuration silently does nothing.
```tsx
import { CratisComponentsProvider } from '@cratis/components';
<CratisComponentsProvider value={{ locale: 'en-US' }} toaster>
<App />
</CratisComponentsProvider>
```
## Step 2 — Pick one of three setups
| Setup | Imports | You own |
| --- | --- | --- |
| **Baseline theme** | `tokens` + `styles` + `theme` | choosing `cratis-dark` / `cratis-light` / system preference; overriding only the brand values you mean to |
| **Product theme** | `tokens` + `styles` + *your* CSS, imported after them | mapping your complete palette onto `--cratis-*`; typography, spacing, motion, contrast; component treatment through parts |
| **Fully owned** | `tokens` + `styles` + your CSS | the same, plus every `--cratis-*` value — omit `theme` entirely |
A theme is **CSS**. No JavaScript preset, provider option or wrapper class is
required for the normal whole-application setup. The product's CSS is written
*outside* a cascade layer, so it wins over all three Components layers
(`cratis-theme`, `cratis-components`, `cratis-utilities`) without specificity
tricks for structural rules. Tokens and theme are unlayered: their overrides
still need the correct specificity. If the product uses layers, name the
`cratis-*` layers in its **first** layer-order statement, before any stylesheet
(see Tailwind below).
### Baseline theme and dark mode
`theme` adds document foreground/background, system dark-mode values, the
explicit scheme classes, forced-colors tuning and `.cratis-theme` subtree
defaults. It stays visually familiar to Components 2/3 — Lara-adjacent blue
actions, neutral surfaces, 6px radii — implemented entirely with Cratis tokens.
```ts
document.documentElement.classList.toggle('cratis-dark', darkMode);
```
- Without a class, the baseline follows `prefers-color-scheme`.
- `cratis-light` keeps light values when the OS prefers dark; an explicit light
wins over an ambient root `cratis-dark`.
- An independently themed island puts `cratis-theme` on the subtree and adds
`cratis-dark` or `cratis-light` there (on the same element, or on an ancestor).
- A plain `:root` override after `theme` loses to dark selectors such as
`:root.cratis-dark`, `.cratis-theme.cratis-dark`, and the system-dark
`:root:not(.cratis-light)`. Match the scheme selectors' specificity and place
overrides after the import, including subtree selectors when using islands.
Or omit `theme` and use the product-theme setup below.
- - Components maps Arc's `--color-error` / `--color-error-bg` in both dark
+ - Requires Components 4.22.1: it maps Arc's `--color-error` / `--color-error-bg` in both dark
schemes. A product theme omitting `theme` must define those variables itself;
do not carry forward the old dark-error contrast workaround.
### Product theme
```css
:root {
--cratis-primary-color: var(--brand-accent-700);
--cratis-primary-color-text: var(--brand-text-inverse);
--cratis-action-background: var(--brand-action);
--cratis-action-background-hover: var(--brand-action-hover);
--cratis-action-background-active: var(--brand-action-active);
--cratis-action-text: var(--brand-on-action);
--cratis-surface-ground: var(--brand-canvas);
--cratis-surface-card: var(--brand-surface);
--cratis-surface-overlay: var(--brand-surface);
--cratis-surface-border: var(--brand-border);
--cratis-control-background: var(--brand-control);
--cratis-control-border: var(--brand-control-border);
--cratis-text-color: var(--brand-text-primary);
--cratis-text-color-secondary: var(--brand-text-secondary);
--cratis-focus-ring: var(--brand-focus-ring);
}
[data-theme='dark'] {
--brand-canvas: #171717; /* switch schemes by redefining the *product* values under the product's own selector */
}
```
Nothing sits between the product tokens and the rendered component — no
renderer preset, no internal selector, no commercial theme package.
## Step 3 — Colors: use the token seam
Never hard-code a hex or `rgb()` value for UI chrome — it breaks the moment the
theme changes. Read a `--cratis-*` custom property instead. The full vocabulary
(from `tokens.css`):
| Group | Tokens |
| --- | --- |
| Accent | `--cratis-primary-color`, `--cratis-primary-color-text`, `--cratis-primary-300` … `-600`, `--cratis-primary-600-text` |
| Primary action | `--cratis-action-background` / `-hover` / `-active`, `--cratis-action-text` |
| Status pairs | `--cratis-info-background` / `-text`, `--cratis-success-*`, `--cratis-warning-*`, `--cratis-danger-*`; single indicators `--cratis-green-500`, `--cratis-orange-500`, `--cratis-red-500` |
| Surfaces | `--cratis-surface-ground` (page), `-section`, `-card`, `-overlay` (dialogs, popovers, toasts), `-hover`, `-border`, `-0`, `-100` |
| Controls | `--cratis-control-background`, `--cratis-control-border`, `--cratis-control-height` / `-small` / `-large` |
| Text & highlight | `--cratis-text-color`, `--cratis-text-color-secondary`, `--cratis-highlight-bg`, `--cratis-highlight-text-color` |
| Effects | `--cratis-focus-ring`, `--cratis-maskbg`, `--cratis-border-radius`, `--cratis-disabled-opacity`, `--cratis-shadow-subtle` / `-overlay` / `-dialog` / `-toast` |
| Layering | `--cratis-z-index-dialog` (1100), `-overlay` (1200), `-filter` (1250), `-tooltip` (1300), `-toast` (1400) |
Only hard-code a color that is intentionally theme-independent — a brand accent
dot, a traffic-light indicator.
## Step 4 — Writing styles
- Put static styles in a **co-located `.css` file**, one per component. A
composition root's CSS carries layout and positioning for its children, not the
children's own styling.
- Use inline `style` **only** for runtime-dynamic values such as computed pixel
positions. A `style` object full of static token names bypasses theming and
review — move it to a class.
- Name classes with a prefix matching the component.
- Import product CSS *after* the Components entries so it wins the cascade.
- When a customer supplies brand material, keep their CSS and tokens verbatim in product-owned files, map them onto the `--cratis-*` tokens, and use the baseline theme for every decision their material does not cover.
## Step 5 — Parts: reaching inside a component
Every meaningful element a component renders carries **`data-cratis-part`**, and
most components accept a **`pt`** prop typed by their own `*Parts` type. They
are documented separately because they are not always the same names: for most
components the typed `pt` key is the camelCase spelling of the kebab-case DOM
value (`headerRow` ↔ `header-row`), but the Toolbar family prefixes its DOM
values (`ToolbarButtonParts.root` ↔ `data-cratis-part='button'`,
`ToolbarFolderParts.root` ↔ `'toolbar-folder'`, `ToolbarFanOutParts.trigger` ↔
`'fanout-trigger'`). Read the DOM value off the rendered element (or the parts
manifest in `@cratis/components/types`) before writing a selector. Both spellings
are stable across internal foundation changes; React Aria class names and
undocumented DOM structure are not.
- **One instance:** `pt` — plain HTML attributes per part.
```tsx
<Dropdown aria-label='Role' options={roles}
pt={{ trigger: { className: 'product-select-trigger' }, popover: { className: 'product-select-popover' }, option: { className: 'product-select-option' } }} />
```
- **Product-wide:** a CSS rule on the part and its state attributes.
```css
[data-cratis-part='header-cell'] { text-transform: uppercase; }
[data-cratis-part='row'][data-selected='true'] { background: var(--product-selected-row); }
.product-dialog[data-cratis-part='root'] { border-radius: 1rem; }
```
The typed part families: `ButtonParts`/`IconButtonParts` (`root`, `icon`,
`label`, `spinner`); `DialogParts` (`backdrop`, `positioner`, `root`, `header`,
`title`, `close`, `content`, `footer`, `confirm`, `cancel`); `DropdownParts`
(`root`, `trigger`, `value`, `clear`, `indicator`, `popover`, `listbox`, `option`,
`filter`, `multiple`); `DataTableParts` (`root`, `search`, `searchInput`,
`tableContainer`, `table`, `head`, `headerRow`, `headerCell`, `body`, `row`,
`cell`, `emptyRow`, `emptyCell`, `loadingRow`, `loadingCell`, `failureRow`,
`failureCell`); `TablePaginatorParts` (`root`, `range`, `info`,
`first`, `previous`, `next`, `last`); `StepperParts` (`root`, `list`, `step`,
`header`, `number`, `title`, `separator`, `panels`, `panel`);
`ToasterPassThrough` (`region`, `toast`, `icon`, `content`, `title`,
`description`, `action`, `close`); the `Toolbar*Parts` family; and per-field
parts for every CommandForm field. The Components `Styling/pass-through`
reference is the exhaustive table.
**Named parts where a component wraps two surfaces** — the usual cause of a
`pt` that "does nothing" is applying it to the wrong one:
- `DataPage`: `tablePt: DataTableParts`, `paginatorPt: TablePaginatorParts`, `menubarPt: ButtonParts`.
- `StepperCommandDialog`: `pt` is the inner **stepper**, `dialogPt` the outer **dialog**.
- `Column`: `filterPt: ColumnFilterMenuParts` for the filter popup.
**State attributes** are component-specific: `data-active`, `data-selected`,
`data-invalid`, `data-disabled`, `data-readonly`, `data-loading`, `data-position`,
`data-orientation`, `data-size`, `data-severity`. Do not assume one exists on
every component — read the component's parts reference.
`ptOptions` and `unstyled` are accepted for Components 3 source compatibility and
do **nothing**: part attributes always merge, and styling is always CSS-owned.
There is no global provider `pt`.
## Renderer adapters — when a vendor look is wanted
The default renderer is built in; omit `library` on the provider. Three optional
adapter packages exist — `@cratis/components.mui` (MUI 9),
`@cratis/components.primereact` (PrimeReact 11), `@cratis/components.primereact10`
— and each adapts exactly **nine primitive slots**: button, icon-button, text
input, text area, checkbox, radio, switch, progress bar (`common.progress`, not
`common.progressBar`), surface. Install and upgrade each adapter at **exactly
the same version** as `@cratis/components`; Node >=22 is required. The broader
renderer contract also has an atomic `common.tooltip` slot accepting any React
node, but it is not one of the nine stable presentation slots. Pass the
adapter's manifest to the provider's `library` prop. Dialogs, dropdowns, tables,
date pickers, tooltips, paginators, toasts and steppers stay Components-owned
regardless; an adapter never restores a vendor's public API, and the vendor's
own provider, theme and license remain the application's, outside Components.
Reach for an adapter only when the product genuinely wants that vendor's look on
those nine controls — mapping tokens is almost always the smaller change.
## Where Tailwind fits
Tailwind is **one supported way to write the CSS above, not the Cratis default.**
It is a fine tool for authoring the token mapping and part rules; it is not a
substitute for them. Components' own internal utilities are prefixed (`cratis:*`)
and are not public styling hooks. `styles` ships no Preflight and no reset of its
own. A host with layers must put this order in its **first** `@layer` statement,
parsed before any stylesheet (for example an inline `<style>` in the host):
```css
@layer properties, theme, base, cratis-theme, cratis-components, cratis-utilities, components, utilities;
```
Components layers belong above the product reset and below product utilities.
Declaring them after the imports cannot reorder layers already established;
otherwise component rules beat product utilities and `pt` classes lose.
## Verify
- No PrimeReact / PrimeIcons / PrimeUI package remains in the manifest unless the
application itself imports it directly — and then it has its own provider.
- `CratisComponentsProvider` (root import) wraps the tree with locale/messages;
exactly one toaster is mounted when notifications are used.
- `@cratis/components/tokens` and `@cratis/components/styles` are imported once
at the entry point, tokens first; `theme` is imported or deliberately omitted;
product CSS comes after.
- No hex or `rgb()` value is hard-coded for UI chrome; colors read `--cratis-*`.
- No CSS targets a React Aria class name or undocumented DOM; parts are reached
through `pt` or `[data-cratis-part=…]`.
- Inline `style` carries only runtime-dynamic values.
- Named parts land on the intended surface (`tablePt` vs `menubarPt`; `pt` vs
`dialogPt`).
- Dark mode is toggled with `cratis-dark` / `cratis-light` on the root element
(or a `cratis-theme` subtree), not through a provider option.