Joy DOM

API reference

Props, types, and error behavior for @joy-dom/react.

<Joy> (component)

The primary entry point. Renders a Joy DOM document as React elements.

<Joy spec={spec} components={{ "contact-button": ContactButton }} />

Props (JoyProps)

PropTypeRequiredDescription
specSpec | TemplateSpecYesThe Joy DOM document. Directives resolve against the document's state on every render.
initialStateJsonNoOverrides the template's state region as the state seed.
directivesDirectiveRegistryNoDirective registry replacing the default $event/$get/$if/$for pack.
componentsRecord<string, JoyComponent>NoCustom node registry, keyed by kebab-case type name.
onEventJoyEventHandlerNoListener for event actions reaching the host. Mutations never arrive here.
onErrorJoyErrorHandlerNoListener for interactions that stop on a typed ActionError. Without it, errors log.
responsive"media" | "container"NoHow breakpoints resolve: viewport @media (default) or @container against its own box.
refRef<HTMLDivElement>NoRef to the host div that holds the shadow root, useful for measuring or screenshotting.

<Joy> renders into its own shadow root, so the document's global ids and classes stay isolated from the host page.

Types

JoyProps

The document owns its state: seeded from the template's state region (or initialState), changed only by actions. See Templates and state.

type JoyProps = JoyRenderOptions & {
  /** Ref to the host element that holds the shadow root. */
  ref?: Ref<HTMLDivElement>;
  spec: TemplateSpec | Spec;
  /** Overrides the template's `state` seed. */
  initialState?: Json;
  directives?: DirectiveRegistry;
};

JoyRenderOptions

The options <Joy> accepts alongside spec:

type JoyRenderOptions = {
  components?: Record<string, JoyComponent>;
  onEvent?: JoyEventHandler;
  onError?: JoyErrorHandler;
  renderNode?: NodeRenderer;
  responsive?: "media" | "container";
};

JoyComponent<N>

A custom component:

type JoyComponent<N extends Node = Node> = ComponentType<JoyComponentProps<N>>;

JoyComponentProps<N>

What custom components receive: RenderedNodeProps plus the rendered children and the original node.

type JoyComponentProps<N extends Node = Node> = RenderedNodeProps & {
  children?: ReactNode;
  node: N;
};
PropWhat's in it
childrenRendered child nodes and primitive values.
nodeThe raw Joy DOM node. Read it for anything the renderer doesn't surface directly.
idResolved node id (from props.id).
classNameThe document's class list for the node, joined into a string.
styleResolved inline style. Built-in nodes only; undefined for custom types.
altimg-only. The resolved alt attribute.
srcimg-only. The resolved src attribute.

For a custom node, style, alt, and src stay unset. Style it from the cascade via className. The on* handlers are passed whenever the node binds a matching action, and the component decides which element fires them.

RenderedNodeProps

The bag of props the renderer passes to every node (text, image, block, or custom):

type RenderedNodeProps = {
  alt?: string;
  id?: string;
  className?: string;
  src?: string;
  style?: CSSProperties;
  onClick?: (event: SyntheticEvent) => void;
  onFocus?: (event: SyntheticEvent) => void;
  onBlur?: (event: SyntheticEvent) => void;
  onChange?: (event: SyntheticEvent) => void;
};

The on* handlers are wired on nodes whose props carry a matching event reference. Each one runs the named handler from the document's root handlers region as one interaction: mutations change the document's state, event actions (and references with no matching handler) reach onEvent. Custom components receive the handlers and choose where to attach them.

JoyEventHandler

Passed as onEvent. It fires for every event action that reaches the host, after the interaction's state changes have applied:

type JoyEvent = {
  name: string;
  params: Record<string, Json>;
  /** The node whose binding raised the event; absent for host-dispatched actions. */
  node?: Node;
};

type JoyEventHandler = (event: JoyEvent, handle: JoyHandle) => void;

Branch on event.name; event.params is the action's params with directive reads resolved.

JoyHandle

The host's way back into the document. Anything done through it runs as a new interaction, queued after the one that raised the event:

type JoyHandle = {
  dispatch: (action: Action) => void;
  /** Snapshot of the settled state, a copy safe to keep or mutate. */
  getState: () => Json;
};

JoyErrorHandler

Passed as onError. Receives the typed ActionError when an interaction stops early. It carries identity (what went wrong), verb, path, and at (the failing action's position in nested sequences):

type JoyErrorHandler = (error: ActionError) => void;

renderSpec (function)

Stateless render of an already-resolved Spec, with no runtime and no directives. Bound actions reach options.dispatch when their event fires. Without it, bindings stay inert.

function renderSpec(spec: Spec, options?: JoySpecOptions): ReactElement;

Use it for static server rendering or test harnesses; <Joy> is the stateful entry point.

Error behavior

  • A template fails to resolve. The resolve pass throws a ResolveError from @joy-dom/core, carrying identity (what went wrong) and marker (which directive). Missing state data does not throw; it reads as null and the render degrades.
  • An interaction fails. A mutation hitting the wrong shape stops its interaction with a typed ActionError, reported through onError (or the console). Applied actions stay applied; the render never crashes on one.
  • A document fails SpecSchema validation at the boundary you control. @joy-dom/react does not call SpecSchema.parse itself, so validate before passing.
  • A document references a custom node with no entry in components. The renderer throws. This is intentional. Silent fallback hides the real bug.
  • Check a document before rendering with validateSpec(spec) from @joy-dom/core. It returns { path, message } findings for everything the renderer would refuse (e.g. a container with children and no explicit display), so tools can report instead of crash.
  • A malformed padding, margin, or borderRadius shape is dropped silently. Validate upstream with SpecSchema to catch it.

Why doesn't the renderer validate?

The renderer trusts its input. Validation is a step you choose to run at the boundary (network, file load). Running it on every render would be wasteful. See Tutorial › Publishing for the recommended pattern.

Where next

  • Tutorial, build a document from scratch, one chapter at a time.
  • Specification, the document format this renderer consumes.

On this page