@cratis/components
v2.3.0
Published
A collection of React components for building modern applications with Cratis.
Downloads
1,474
Readme
Cratis Components
A collection of React components for building modern applications with Cratis.
Requirements
Minimum Versions
- TypeScript: 4.7+
- React: 18.0+ or 19.0+
- Node.js: 16+ (for development)
TypeScript Configuration
This package is compatible with all modern TypeScript moduleResolution strategies:
- ✅
"bundler"(recommended for Vite, esbuild, webpack 5+) - ✅
"node16"/"nodenext"(for Node.js projects) - ✅
"node"(legacy, but supported)
The package provides dual CommonJS and ES Module builds with proper conditional exports for optimal module resolution and tree-shaking.
Installation
npm install @cratis/components primereact primeicons
# or
yarn add @cratis/components primereact primeiconsprimereact and primeicons are peer dependencies — installing them in your
app ensures a single copy is shared with the wrappers in this package. The
@cratis/arc* packages and react/react-dom are also peer dependencies;
you typically already have them.
The following are optional peer dependencies, only required if you use the component that depends on them:
| Component | Optional peer |
|---|---|
| PivotViewer | pixi.js (canvas) and framer-motion (animated panels) |
| DataPage resizable layout | allotment |
Install them only when you reach for the corresponding component.
Usage
Importing Components
You can import components using subpath imports for better tree-shaking:
// Import specific component modules
import { TimeMachine } from '@cratis/components/TimeMachine';
import { DataPage } from '@cratis/components/DataPage';
import { CommandForm } from '@cratis/components/CommandForm';
// Or import from the main entry point
import { TimeMachine, DataPage } from '@cratis/components';Available Subpath Exports
Components:
@cratis/components— package root (re-exportsCratisComponentsProviderand the namespaced component groups)@cratis/components/CommandDialog@cratis/components/CommandStepper@cratis/components/CommandForm@cratis/components/CommandForm/fields@cratis/components/Common@cratis/components/DataPage@cratis/components/DataTables@cratis/components/Dialogs@cratis/components/Dropdown@cratis/components/ObjectContentEditor@cratis/components/ObjectNavigationalBar@cratis/components/PivotViewer@cratis/components/SchemaEditor@cratis/components/TimeMachine@cratis/components/Toolbar@cratis/components/types
Stylesheets:
@cratis/components/styles— Tailwind utilities + Cratis CSS variable tokens (single stylesheet, recommended)@cratis/components/tokens— only the--cratis-*CSS variable tokens (for consumers using their own utility CSS solution)
Styling
This package ships primarily for its functionality and Arc integrations. Styling is designed to stay out of the way: choose the setup that matches how much control you want, and the other layers stay invisible.
Tip — see each setup live: every Storybook story includes a Styling toolbar (paintbrush icon) that flips between five modes that demonstrate the three setups below: Lara Dark Blue, Lara Light Blue, Themed with custom palette, Unstyled (bare structure), and Unstyled + Tailwind pt. Open any story (
yarn dev) and switch modes to see the same component under each setup.
TL;DR — choose a styling setup
| Setup | When | Effort | What you write |
|---|---|---|---|
| Use a PrimeReact theme | You want components to look good immediately and tweak from there. | Lowest | Theme CSS import + provider |
| Use a custom palette on top of a PrimeReact theme | You want PrimeReact's structure but your own colors. | Low | A PrimeReact theme + CSS variable overrides |
| Use fully unstyled mode | You're integrating into a tightly controlled design system. | Highest | unstyled: true + a pt preset in CSS or Tailwind |
Why the first two options still load a PrimeReact theme
In PrimeReact 10, every widget's structural CSS (padding, borders, dialog frame, focus rings, button shapes) ships inside the theme file. There is no separate "primitives" stylesheet. So a consumer who doesn't load any PrimeReact theme also has no structural CSS — components render as their raw HTML primitives.
The
--cratis-*token layer is therefore an additive Cratis-scoped tint for surfaces our wrappers own (validation error text, the FormElement addon, breadcrumb borders, etc.). It is not, by itself, enough to skin PrimeReact widgets. Override PrimeReact's variables when you want the whole UI in your palette. Useunstyled: trueand aptpreset when you want to replace PrimeReact's visuals entirely.
All three setups use the same one-line setup. You can change direction later
because the same provider, tokens, and pt hooks stay available.
One-line setup (every styling option)
import '@cratis/components/styles';
import { CratisComponentsProvider } from '@cratis/components';
export const App = () => (
<CratisComponentsProvider>
<YourApp />
</CratisComponentsProvider>
);@cratis/components/stylesships the Tailwind utility classes used inside the package plus the--cratis-*CSS variable token layer that every internal component reads from. (Use@cratis/components/tokensinstead if you're bringing your own Tailwind.)CratisComponentsProvideris a thin wrapper over PrimeReact'sPrimeReactProviderso Cratis has one place to layer in defaults. Drop in rawPrimeReactProviderif you'd rather.
The three setups below differ only in what else you load on top of this setup.
Use a PrimeReact theme
Load any PrimeReact theme stylesheet alongside Cratis Components. PrimeReact's
own widgets paint themselves from the theme, and the --cratis-* tokens cascade
to the matching theme variables so Cratis-scoped surfaces follow along.
// 1. Theme first, then Cratis styles so any --cratis-* override wins.
import 'primereact/resources/themes/lara-dark-blue/theme.css';
import 'primeicons/primeicons.css';
import '@cratis/components/styles';
import { CratisComponentsProvider } from '@cratis/components';
export const App = () => (
<CratisComponentsProvider>
<YourApp />
</CratisComponentsProvider>
);Override a single component with CSS
Plain CSS works fine on top of the theme. Target either PrimeReact's class
names or your own className:
/* yourApp.css */
.p-button {
border-radius: 999px; /* pill buttons everywhere */
}
.dangerous-button {
background: var(--cratis-red-500);
color: white;
}<Button label="Delete" className="dangerous-button" />Override a single component with Tailwind
Pass Tailwind utility classes through the wrapper's className prop:
<InputTextField value={c => c.name}
className="rounded-2xl bg-slate-900 text-slate-50" />
<Dialog title="Confirm" className="shadow-2xl rounded-3xl">
{/* … */}
</Dialog>Use this setup when: you're prototyping, building internal tools, or are happy with one of the prebuilt PrimeReact themes.
Use a custom palette on top of a PrimeReact theme
Keep a PrimeReact theme as your structural baseline (so every widget gets
its padding, dialog frame, button shape, focus ring, etc.) and override the
PrimeReact CSS variables on :root to repaint the whole UI in your own
colors. The --cratis-* tokens follow along through tokens.css's cascade, so
Cratis-scoped surfaces stay in sync — and you can override the Cratis tokens
independently if you want Cratis surfaces to differ from PrimeReact widgets.
With plain CSS
/* palette.override.css — imported once, after @cratis/components/styles */
:root {
/* PrimeReact variables — these are what PrimeReact widgets read. */
--surface-0: #1e293b;
--surface-100: #1e293b;
--surface-ground: #020617;
--surface-section: #0f172a;
--surface-card: #1e293b;
--surface-overlay: #1e293b;
--surface-hover: #334155;
--surface-border: #334155;
--text-color: #f8fafc;
--text-color-secondary: #94a3b8;
--primary-color: #38bdf8;
--primary-color-text: #0b1220;
--highlight-bg: #1e40af;
--highlight-text-color: #ffffff;
--border-radius: 10px;
/* --cratis-* tokens default to var(--surface-*) etc. via tokens.css, so
the overrides above flow through automatically. Set these explicitly
only if you want Cratis-scoped surfaces tinted differently. */
--cratis-red-500: #ef4444;
--cratis-green-500: #22c55e;
}// 1. PrimeReact theme provides the structure.
import 'primereact/resources/themes/lara-dark-blue/theme.css';
import 'primeicons/primeicons.css';
import '@cratis/components/styles';
// 2. Your palette overrides — must come after the theme so they win.
import './palette.override.css';Scoped (dark-on-light, light-on-dark, etc.)
PrimeReact variables cascade like any other CSS variable, so an ancestor scope works:
.dark-zone {
--surface-card: #0b1220;
--text-color: #f8fafc;
--primary-color: #60a5fa;
}<div className="dark-zone">
<Dialog title="Always dark">…</Dialog>
</div>With Tailwind CSS
Tailwind's @layer base is the idiomatic spot — declare the palette once and
Tailwind handles cascade and dark mode:
/* app.css */
@import "tailwindcss";
@import "primereact/resources/themes/lara-dark-blue/theme.css";
@import "@cratis/components/styles";
@layer base {
:root {
--surface-card: theme('colors.slate.800');
--surface-border: theme('colors.slate.700');
--text-color: theme('colors.slate.50');
--primary-color: theme('colors.sky.400');
--cratis-red-500: theme('colors.red.500');
}
.dark {
--surface-card: theme('colors.slate.900');
--text-color: theme('colors.slate.100');
}
}What --cratis-* tokens are for
PrimeReact widgets read PrimeReact's own variables (--surface-card,
--text-color, --primary-color, …) directly. Cratis wrappers add some
surfaces of their own (inline validation error text, the FormElement addon
background, the breadcrumb bottom border, etc.) — those use a parallel set
of --cratis-* tokens that default to the PrimeReact value via the cascade
defined in tokens.css.
The upshot:
- Override PrimeReact variables to repaint the whole UI (PrimeReact widgets + Cratis surfaces).
- Override
--cratis-*tokens when you specifically want Cratis surfaces to differ from PrimeReact widgets.
--cratis-* token reference (Cratis-scoped surfaces)
| Group | Tokens |
|---|---|
| Surfaces | --cratis-surface-0, --cratis-surface-100, --cratis-surface-ground, --cratis-surface-section, --cratis-surface-card, --cratis-surface-overlay, --cratis-surface-hover, --cratis-surface-border |
| Text | --cratis-text-color, --cratis-text-color-secondary |
| Brand | --cratis-primary-color, --cratis-primary-color-text, --cratis-primary-300, --cratis-primary-400, --cratis-primary-500, --cratis-primary-600 |
| Selection | --cratis-highlight-bg, --cratis-highlight-text-color |
| Semantic | --cratis-green-500, --cratis-orange-500, --cratis-red-500 |
| Geometry | --cratis-border-radius |
| Effects | --cratis-focus-ring, --cratis-maskbg |
Each defaults to the PrimeReact variable with the same name minus the
--cratis- prefix (e.g. --cratis-surface-card → var(--surface-card)).
Use this setup when: you want a custom look without writing a PrimeReact theme from scratch, you're shipping multiple palette variants (light/dark/ brand), or you want Cratis-scoped surfaces tinted differently from PrimeReact widgets.
Use fully unstyled mode
Turn off every PrimeReact base style at the provider and supply visuals
through PrimeReact's pt (pass-through) mechanism, your own CSS, or both.
Components render structurally only and become a blank canvas.
import '@cratis/components/styles'; // tokens + Tailwind utilities still useful for spacing/layout
import { CratisComponentsProvider } from '@cratis/components';
export const App = () => (
<CratisComponentsProvider value={{ unstyled: true, pt: globalPt }}>
<YourApp />
</CratisComponentsProvider>
);A pt preset in plain CSS
Attach a className from your own stylesheet via a global preset:
// pt-preset.ts
export const globalPt = {
button: {
root: { className: 'my-btn' },
},
dialog: {
root: { className: 'my-dialog' },
header: { className: 'my-dialog__header' },
content: { className: 'my-dialog__body' },
},
inputtext: {
root: { className: 'my-input' },
},
} as const;/* yourApp.css */
.my-btn {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
background: var(--cratis-primary-color);
color: var(--cratis-primary-color-text);
border: none;
border-radius: var(--cratis-border-radius);
cursor: pointer;
}
.my-dialog__header {
padding: 1rem 1.25rem;
background: var(--cratis-surface-card);
border-bottom: 1px solid var(--cratis-surface-border);
font-weight: 600;
}A pt preset in Tailwind
Same shape, Tailwind utilities as the class strings:
// pt-preset.ts
export const globalPt = {
button: {
root: { className: 'inline-flex items-center px-4 py-2 rounded-lg bg-sky-500 text-white hover:bg-sky-400 disabled:opacity-50' },
},
dialog: {
root: { className: 'rounded-2xl shadow-2xl overflow-hidden' },
header: { className: 'px-5 py-3 bg-slate-800 text-slate-50 font-semibold border-b border-slate-700' },
content: { className: 'p-5 bg-slate-900 text-slate-100' },
},
inputtext: {
root: { className: 'w-full px-3 py-2 rounded-md bg-slate-800 text-slate-50 border border-slate-700 focus:border-sky-400 focus:outline-none' },
},
} as const;Per-instance overrides
Anything global can be overridden per-instance — useful when one component needs to look different:
<Dialog
title="Brand callout"
pt={{ root: { className: 'rounded-none' },
header: { className: 'bg-pink-600 text-white' } }}>
…
</Dialog>
<InputTextField value={c => c.name}
pt={{ root: { className: 'border-2 border-pink-500' } }} />Composite components in unstyled mode
DataPage and StepperCommandDialog compose multiple PrimeReact widgets and
expose explicit per-slot props. The global pt reaches every internal widget;
per-instance overrides target the inner slot directly:
<DataPage<AllAuthors, Author, never>
title="Authors" query={AllAuthors}
tablePt={{ table: { className: 'min-w-full divide-y divide-slate-700' } }}
menubarPt={{ root: { className: 'px-3 py-2 bg-slate-900' } }}>
<DataPage.MenuItems>…</DataPage.MenuItems>
<DataPage.Columns>…</DataPage.Columns>
</DataPage>
<StepperCommandDialog<RegisterOrder> command={RegisterOrder} title="New order"
/* pt targets the Stepper */
pt={{ stepperpanel: { content: { className: 'pt-6' } } }}
/* dialogPt targets the outer Dialog */
dialogPt={{ header: { className: 'bg-slate-900' } }}>
…
</StepperCommandDialog>ObjectContentEditor, ObjectNavigationalBar, and SchemaEditor accept only
className on the root — restyle their internals via the global pt
preset.
Use this setup when: you have a design system to honor, you're matching a brand kit, or you want zero PrimeReact CSS in the final bundle.
Combining styling setups
The styling options compose, so you don't have to choose one for the whole app:
- Themed with one unstyled component — keep the PrimeReact theme and pass
unstyledper-component to opt that one widget out:<Dialog title="Custom" unstyled pt={brandDialogPt}>…</Dialog> - Unstyled with one themed island — wrap a subtree in a second
CratisComponentsProviderthat restores defaults:<CratisComponentsProvider value={{ unstyled: true, pt: globalPt }}> <App /> <CratisComponentsProvider value={{ unstyled: false }}> <PrimeReactThemedSubtree /> </CratisComponentsProvider> </CratisComponentsProvider> - Dark mode — scope the palette overrides to
.dark(override--surface-card,--text-color,--primary-color, etc., plus any--cratis-*tokens you want to diverge) and toggle the class on the root element. PrimeReact widgets and Cratis surfaces both follow the cascade.
Per-component pt cheat sheet
Three patterns, depending on how much PrimeReact a wrapper composes:
- Single-widget wrappers —
Dialog, everyCommandFormfield,EventsView, andDropdownforwardpt,ptOptions,unstyled, andclassNamestraight to their inner PrimeReact component. - Multi-slot composites —
StepperCommandDialog(ptfor Stepper,dialogPtfor Dialog),DataPage(tablePtfor DataTable,menubarPtfor Menubar), andDataTableForQuery/DataTableForObservableQuery(ptfor DataTable,paginatorPtfor Paginator) — each slot has*PtOptions,*Unstyled, and (where applicable)*ClassNamesiblings. - Large composites —
ObjectContentEditor,ObjectNavigationalBar,SchemaEditorexposeclassNameonly; restyle internals via the globalptpreset.
What is not fully pass-through
A small number of internal usages opt into PrimeReact's slot-rendering by
name (for example, a custom Menubar item template uses p-menuitem-link /
p-menuitem-text to match the surrounding default-rendered items). These are
correct contracts with PrimeReact's own slot rendering, not hard-coded
theming — they have no effect in unstyled mode and match the rest of the
menu in themed mode.
BusyIndicatorDialog only honors the global pt set via
CratisComponentsProvider; it does not accept per-instance pt because its
request type is owned by @cratis/arc.react.
Troubleshooting
Module Resolution Errors
If you encounter errors like:
Cannot find module '@cratis/components/TimeMachine' or its corresponding type declarations.Solution: Ensure you're using the correct case-sensitive import paths (e.g., TimeMachine, not timeMachine).
If using TypeScript 4.7+, try updating your tsconfig.json:
{
"compilerOptions": {
"moduleResolution": "bundler" // or "node16" / "nodenext"
}
}Import Errors
Ensure you're using the correct import paths. The package uses case-sensitive paths that match the actual component names.
