Authoring a new built-in CraftJS component

Are you a host app developer wanting to add your own draggable component? You don't need this doc. Use [defineComponent + PageHubConfig.components](registration-host.md) — one file, one function call, no SDK fork. This doc is for SDK con…

Are you a host app developer wanting to add your own draggable component? You don't need this doc. Use defineComponent + PageHubConfig.components — one file, one function call, no SDK fork.

This doc is for SDK contributors adding a new built-in that ships in BUILTIN_COMPONENT_DEFS alongside Container / Text / Button.

A new built-in touches 8 places inside packages/sdk/src. Most are one-line registrations. The real work is the <Name>.craft.tsx file (#2) — that's where defaults, the inspector schema, presets, and the toHTML serializer live.

The 8 places

#FileWhat
1components/<Name>.tsxThe React component.
2components/<Name>.craft.tsxdefineComponent(...) with toHTML for static export. The real work.
3chrome/toolbar/inspector/mainTabs/<Name>MainTab.tsxSettings panel (and <Name>MainTabAdvanced if needed).
4components/definitions.tsexport { <Name>Def } from "./<Name>.craft".
5components/resolvers/viewer.tsAdd <Name>: cv(<Name>) to viewerResolver. Single source of truth — DEFAULT_CRAFT_RESOLVER (editor) derives from it via spread + SavedComponentLoader. Used by /view/, /static/, /build/.
6core/componentRegistry.tsAdd <Name>Def to BUILTIN_COMPONENT_DEFS (def list for toolbox / static HTML / viewer processing). Resolver map is now derived — no edit needed there.
7define.tsAdd the name to BUILT_IN_NAMES so collision detection works for host-app custom components.
8index.tsexport { <Name> } from "./components/<Name>".

After SDK changes, hard-reload the editor — Turbopack HMRs but the running React tree caches the resolver map.

Common omission patterns (have shipped before)

  • Forget viewer.ts → both editor and published pages crash with Cannot destructure 'type' of ue() is undefined (single resolver source).
  • Forget BUILT_IN_NAMES → a host-app custom component with the same name silently overrides yours.

Don't form a circular import via low-level utilities

Anywhere reachable from low-level utilities a CraftJS component imports (e.g. utils/sanitizeNodeMap.ts, utils/tailwind/tailwind.ts, utils/cloneState.ts, components/selectors.ts) MUST NOT static-import BUILTIN_COMPONENT_DEFS, components/definitions.ts, or any *.craft.tsx. It forms a TDZ cycle that crashes at runtime with ReferenceError: Cannot access '<Component>' before initialization (whichever component is alphabetically first in definitions.ts). pnpm typecheck will not catch it.

Use the registry setter pattern instead — setSanitizeBuiltinDefs style, same shape as registerClientDataFetcher.

Related

  • registration-host.mdhost-app custom components via defineComponent (the doc for SDK consumers, not contributors).
  • rendering — viewer vs editor resolvers (why two lists).