# Integration Guide
How to embed the editor in your own React app.
The shipped artifact is a Vite library-mode bundle: an ES module exporting
`` plus the full SDK. React, React DOM, and Craft.js are
externalized as peer dependencies — your host app provides them.
This guide assumes a Vite + React 19 host (the editor's own development
target). Older React versions are not supported — the React 19 ref-as-prop
semantics + the unified `Fragment` are load-bearing.
## Install
```bash
npm install @crafted-design/editor react@19 react-dom@19 @craftjs/core@^0.2.12
```
`@mui/material`, `@emotion/react`, and `@emotion/styled` are **optional peer
dependencies** — they are NOT bundled. Install them only if you use the full
`@crafted-design/editor` entry (which registers the MUI adapter) or import
`/adapters/mui`; the lean `/core` entry (shadcn + plain-HTML) needs no extra
peers. See *Subpath exports* below.
## Subpath exports
Since `0.7.0` the package is modular — pick the entry that matches the
adapters you want, so you don't bundle a UI library you never render:
| Import path | What you get | External peers |
|---|---|---|
| `@crafted-design/editor` | **Full** `` — registers editor + shadcn + plain-HTML **+ MUI**. The batteries-included default. | requires `@mui/material`, `@emotion/react`, `@emotion/styled` |
| `@crafted-design/editor/core` | **Lean** `` — registers editor + shadcn + plain-HTML, **no MUI**. Same full export surface (Editor, SDK, stores, doc helpers). | none |
| `@crafted-design/editor/adapters/shadcn` | Side-effect import that registers just the shadcn adapter. | none |
| `@crafted-design/editor/adapters/html` | Registers just the plain-HTML adapter (no UI library). | none |
| `@crafted-design/editor/adapters/mui` | Registers just the MUI adapter. | `@mui/material`, `@emotion/react`, `@emotion/styled` |
| `@crafted-design/editor/sdk` | SDK-only surface (`registerAdapter`, `registerCanonical`, `registerPanel`, `registerTheme`, `registerTemplate`, `registerFontToken`, `useNodeClasses`, all the matching types). No editor UI — use when authoring a canonical / adapter / panel without pulling in ``. | none |
| `@crafted-design/editor/index.css` | Tailwind CSS bundle (global preflight + `:root` tokens). Import once per page; no JS overhead. | none |
| `@crafted-design/editor/index.scoped.css` | Same stylesheet, every rule scoped under `.crafted-design-scope` — for embedding inline in a Tailwind-v4 host without a double preflight / token clobbering. Use *instead of* `index.css`. See [Inline embedding](#inline-embedding-into-a-tailwind-v4-app-170). | none |
Typical setups:
```ts
// shadcn-only host — no MUI in the bundle, nothing extra to install
import { Editor } from '@crafted-design/editor/core'
import '@crafted-design/editor/index.css'
// want MUI too — install the peers, use the full entry
// npm install @mui/material @emotion/react @emotion/styled
import { Editor } from '@crafted-design/editor'
// lean core + opt into one extra adapter explicitly
import { Editor } from '@crafted-design/editor/core'
import '@crafted-design/editor/adapters/mui' // side-effect: registers MUI
```
Opt-in is at the **import boundary**, not at runtime: importing an adapter
registers it before `` mounts. (Registering an adapter after mount
would reshape the provider tree and remount the canvas, so it isn't
supported.) `.d.ts` files ship alongside every JS entry, so TypeScript hosts
resolve types without configuration. See
[ADAPTER_VERSIONING.md](./ADAPTER_VERSIONING.md) for the peer-dependency
policy and [ADAPTER_MATRIX.md](./ADAPTER_MATRIX.md) for per-adapter coverage.
## Bundle format
The package ships **ESM only**, **unminified, with source maps**. There is no
CommonJS/UMD build and no separate `*.min.js` — both are deliberate:
- **ESM-only** avoids the dual-package hazard; modern bundlers and Node ≥ 20
consume ESM directly.
- **Unminified** because you consume the editor through your own bundler,
which minifies the final app. Shipping a parallel minified entry would
double the published surface and the `exports` map for no real benefit, and
the source maps give you readable stack traces in development.
The SDK subpath (`/sdk`) is **side-effect-free**, so a bundler tree-shakes any
authoring symbol you don't import. (Importing `/sdk` registers nothing beyond
the editor's three baseline font tokens — `sans`/`heading`/`mono`.)
## Minimal embed
```tsx
import { Editor } from '@crafted-design/editor'
import '@crafted-design/editor/index.css'
function App() {
return
}
export default App
```
The editor takes 100% of its parent's height (it uses `h-screen` internally).
Wrap in a container if you want it to share screen real estate:
```tsx
function App() {
return (
)
}
```
## Embedding as a controlled component (1.6.0)
By default `` is a self-contained app: it owns its document, persists to
IndexedDB, and shows its own Save/Load chrome. To embed it inside your own UI —
a step in a multi-step form, a drawer, a tab — drive it as a **controlled
component** instead. All of these props are additive and optional; with none
passed, behavior is identical to the minimal embed above.
```tsx
import { Editor, type EditorHandle } from '@crafted-design/editor/core'
import type { EditorDocument } from '@crafted-design/editor/core'
function CardEditor() {
// The host owns the document.
const [doc, setDoc] = useState(seedFromYourBackend)
return (
setDoc(next)} // debounced; persist with JSON.stringify(next)
persistence={false} // never touch the built-in IndexedDB store
hideChrome // drop the Save/Load bar — render your own
/>
)
}
```
| Prop | Type | Effect |
|---|---|---|
| `value` | `EditorDocument \| string` | **Controlled.** The document the editor renders; re-seeds whenever its identity changes. Persistence is forced off. |
| `defaultValue` | `EditorDocument \| string` | **Uncontrolled** one-time seed on mount; edits stay internal, surfaced via `onChange`. Ignored when `value` is set. |
| `onChange` | `(doc: EditorDocument) => void` | Fired (debounced) on every change — structural edits **and** prop/style edits. The same envelope `Export` produces. |
| `onChangeDebounceMs` | `number` | Debounce window for `onChange`. Default `150`. |
| `persistence` | `boolean` | Whether the editor manages its own IndexedDB store/autosave. Default `true`. `value` implies `false`. |
| `hideChrome` | `boolean` | Hide document-management chrome (Save/Load/Import/Export/Share bar, onboarding tour, quota banners, cross-tab watcher). Keeps toolbox + canvas + inspector. |
Both `value` and `defaultValue` accept an `EditorDocument` envelope **or** its
JSON string — each is validated + migrated on the way in, exactly like an
Import. Build a seed without an editor using the headless
[`buildDocument`](./SDK_GUIDE.md), or feed a string straight from your backend.
**No feedback loop.** The natural controlled wiring — `edit → onChange →
setState → new value → re-apply` — does **not** loop: the editor tracks the last
serialized tree and skips re-applying a `value` it already produced. Re-applying
an identical envelope is a no-op.
**Reading on demand (imperative ref).** Redundant with `onChange` but convenient
for a "serialize on click" button without holding the doc in state:
```tsx
const ref = useRef(null)
// …
```
A runnable end-to-end example (controlled `value` + `onChange` + ref + a live
`` preview) lives in
[`examples/controlled-host`](../examples/controlled-host).
> **CSS isolation.** This controlled API removes the persistence/chrome/seed
> machinery. To embed **inline** in an app already running Tailwind v4 (no
> iframe), import the scoped stylesheet — see
> [Inline embedding into a Tailwind-v4 app](#inline-embedding-into-a-tailwind-v4-app-170) below.
## Inline embedding into a Tailwind-v4 app (1.7.0)
The default stylesheet `@crafted-design/editor/index.css` is a full Tailwind v4
build — a global preflight (the `*` reset) + the editor's design tokens.
**Tokens don't clobber yours (1.8.2+).** The editor's document tokens
(`--primary`, `--background`, `.dark`, `[data-theme]`) ship in a cascade layer
(`@layer crafted-design`), so your app's **unlayered** `:root` / `.dark` tokens
always win — importing `index.css` no longer overrides your brand colors
app-wide. (The editor's `--ed-*` *chrome* tokens stay unlayered, but a host has
no `--ed-*` to collide with.) The trade-off: because your `:root` wins
everywhere, the editor **canvas** also inherits your brand tokens, and a
preflight is still global. For full subtree isolation — host tokens never reach
the canvas, no second preflight — use the **scoped** stylesheet instead:
```tsx
// in a Tailwind-v4 host — INSTEAD of index.css:
import '@crafted-design/editor/index.scoped.css'
```
Every rule in it is prefixed with `.crafted-design-scope` (and the editor's
`:root` tokens are rehomed onto that class), which `` and
`` put on their root. So:
- The editor's preflight resets **only inside** the editor subtree — your page
isn't double-reset.
- The editor's tokens live **only inside** `.crafted-design-scope` — your host's
`:root` / `--color-*` tokens are untouched, and vice-versa.
- Runtime overlays (Modal/Drawer/Toast) portal into a scope-classed container,
so they're styled correctly even though they're DOM-detached.
**When to use which:**
| Host | Stylesheet |
|---|---|
| Want the editor canvas fully isolated from host tokens (and no double preflight) | `index.scoped.css` |
| Fine with the canvas inheriting your brand tokens; just don't want your `:root` clobbered | `index.css` (global — tokens are layered) |
| No CSS framework / standalone / its own route / iframe | `index.css` (global) |
Notes & limits:
- The scoped sheet **omits a global preflight** — it assumes the host already
has one (Tailwind v4). A host with no reset at all should use `index.css`.
- The editor owns its look: scoping makes the editor's utilities
un-overridable by host CSS (intended). Theme the **canvas** via `registerTheme`
and the **chrome** via `editorTheme`, not by overriding editor utilities.
(`editorTheme` works under the scoped sheet too — the `--ed-*` chrome tokens
stay global so the prop's inline values still apply; 1.8.3+.)
- The **MUI** adapter renders overlays via MUI's own portals (emotion-styled),
outside the Tailwind scoped sheet; the scoped sheet targets the shadcn / html
(Tailwind) stacks.
`examples/controlled-host` embeds inline with the scoped sheet.
## Pinning the adapter (host-chosen design system)
The product model is that **you** — the host — choose the design system; the
people using your editor don't. Pin it with the `adapter` prop:
```tsx
import { Editor } from '@crafted-design/editor' // full entry registers MUI
import '@crafted-design/editor/index.css'
function App() {
return
}
```
What pinning does:
- The active adapter is set to `mui` before first paint.
- The **AdapterSwitcher disappears** from the toolbar — end users can't change
the design system.
- **Loading a document does not override it.** A document saved under shadcn
still opens — documents store canonical ids, not library components, so it
simply renders through MUI. The envelope's `adapterId` is a preference, not
a command, while pinned.
> ⚠ **MUI requires its peers.** The MUI adapter (whether via the full entry or
> `/adapters/mui`) needs the optional peer dependencies installed:
>
> ```bash
> npm install @mui/material @emotion/react @emotion/styled
> ```
>
> Pinning `adapter="mui"` without registering the MUI adapter (or without the
> peers, which makes its import fail) logs a console warning and falls back to
> the default `shadcn`.
Want to pin a starting adapter but still let users switch? Both knobs are
independent:
```tsx
// starts on plain HTML, switcher stays
// default adapter (shadcn), no switcher
// legacy behavior: switcher shows all registered adapters
```
`allowUserToSwitchAdapter` defaults to `false` when `adapter` is set, `true`
otherwise.
## Customizing the registry
The editor pre-registers 48 canonicals, the built-in adapters (shadcn + MUI +
plain-HTML on the full entry; shadcn + plain-HTML on `/core`), 7 themes,
inspector panels, and starter templates. Override any of these by calling the
SDK BEFORE rendering ``:
> **Adapter coverage policy.** The three built-in adapters (shadcn, MUI,
> plain-HTML) implement **every** canonical — see
> [ADAPTER_MATRIX.md](./ADAPTER_MATRIX.md). The in-repo Chakra adapter is an
> *example* (a third-party-adapter demo covering a 20-canonical subset) and is
> NOT part of the published package. When a document uses a canonical the
> active adapter doesn't implement, the node renders a labeled placeholder
> (` — no impl in adapter ""`) instead of crashing, so you
> can swap adapters or remove the node.
### Remove a built-in canonical
```tsx
import { Editor, unregisterCanonical } from '@crafted-design/editor'
unregisterCanonical('alert') // drops Alert from the toolbox
function App() {
return
}
```
### Add a custom canonical
```tsx
import { z } from 'zod'
import { Editor, registerCanonical } from '@crafted-design/editor'
registerCanonical({
id: 'callout',
category: 'feedback',
displayName: 'Callout',
tags: ['alert', 'banner'],
isCanvas: true,
styleSlots: ['root'],
propsSchema: z.object({
intent: z.enum(['info', 'warning', 'success']),
}),
defaults: {
props: { intent: 'info' },
style: { classes: { root: 'p-4 rounded-md border' } },
},
})
// (Add adapter impls for your supported adapters too.)
```
### Add a custom adapter
```tsx
import { Editor, registerAdapter, type AdapterRenderProps } from '@crafted-design/editor'
function MyBox({ children, rootRef, className }: AdapterRenderProps) {
return