React
Install, embed, and reference for the @joy-dom/react renderer.
@joy-dom/react renders a Joy DOM JSON document into React elements. It powers the live previews you see on the spec page.
Install
bun add @joy-dom/react @joy-dom/spec@joy-dom/spec is a peer dependency. It exports the JSON Schema and TypeScript types.
First render
import { } from "@joy-dom/react";
import type { Spec } from "@joy-dom/spec";
const : Spec = {
: 1,
: {
".hello": { : "flex", : { : 16, : "px" } },
: { : "flex" },
},
: [],
: {
: "div",
: { : ["hello"] },
: [{ : "p", : ["Hello Joy DOM"] }],
},
};
export function () {
return < ={} />;
}Drop <JoyDom> anywhere you'd put a regular React component.
Custom components
If the document references custom nodes (kebab-case type), pass them via the components prop:
import { , type } from "@joy-dom/react";
import type { Spec } from "@joy-dom/spec";
function ({ , , , }: ) {
return (
< ={} ={} ={}>
{}
</>
);
}
export function ({ }: { : Spec }) {
return < ={} ={{ "contact-button": }} />;
}The component receives the resolved children, className, style, id, and the original node. See §7.1 Component props.
Missing a registration for a custom node throws at render time.
Events and actions
A node binds an event to a named action in its props (see §8 Events and actions). Pass an actions map keyed by action name; each handler receives the DOM event and the binding's params:
import { , type } from "@joy-dom/react";
import type { Spec } from "@joy-dom/spec";
const : Spec = {
: 1,
: { ".buy": { : "flex" } },
: [],
: {
: "div",
: {
: ["buy"],
: { : "action", : "checkout", : { : "joy-01" } },
},
: ["Buy now"],
},
};
const : = ({ , }) => {
.();
.("checkout", ?.);
};
export function () {
return < ={} ={{ }} />;
}A binding that names an action absent from the map throws at render, so a typo surfaces immediately. Pass missingAction="ignore" to leave such events unbound and render the rest of the tree.
The renderer wires bindings on built-in nodes only. A custom component receives the raw node, so it reads any bindings from node.props and decides how to use them. The interactive widgets use case walks through a card that mixes both.
Load documents at runtime
When the document isn't part of your build:
import { , } from "react";
import { } from "@joy-dom/react";
import { , type Spec } from "@joy-dom/spec";
export function ({ }: { : string }) {
const [, ] = <Spec | null>(null);
(() => {
()
.(() => .())
.(() => .())
.();
}, []);
if (!) return null;
return < ={} />;
}Calling SpecSchema.parse at the boundary makes malformed documents fail fast with a readable Zod error.
Server-side rendering
<JoyDom> renders into its own shadow root. On the server that's a declarative
shadow root (<template shadowrootmode>); the browser attaches it while parsing
the HTML, which means React can't hydrate that subtree afterwards. So pick one:
Static / string rendering. renderToStaticMarkup(<JoyDom spec={spec} />) produces the
declarative shadow root; mount it with element.setHTMLUnsafe(html) so the browser attaches
it. No hydration involved — ideal for static generation or a server function returning markup.
Inside a hydrated app (Next.js, Waku, Remix). Render <JoyDom> on the client so its shadow
root is attached client-side, not hydrated. The simplest way is to gate it on a mounted flag
(const [m, setM] = useState(false); useEffect(() => setM(true), [])) and render {m ? <JoyDom … /> : null}. Joy DOM's own website does exactly this for its previews.
Common integration patterns
Static documents bundled with the app. Import JSON directly: import card from "./card.json". Bundlers tree-shake; production payload only carries the documents you
reference.
SpecSchema as above.Per-user generated documents. Render on the server, pass to the client as initial props. Validate on the server boundary, not the client (cheaper, single source of truth).
API reference
<JoyDom> (component)
The primary entry point. Renders a Joy DOM document as React elements.
<JoyDom spec={spec} components={{ "contact-button": ContactButton }} />Props (JoyDomProps)
| Prop | Type | Required | Description |
|---|---|---|---|
spec | Spec | Yes | The Joy DOM document. |
components | Record<string, JoyDomComponent> | No | Custom node registry, keyed by kebab-case type name. |
actions | Record<string, ActionHandler> | No | Handlers for the document's event action bindings, keyed by action name. |
missingAction | "throw" | "ignore" | No | What to do when a binding names an absent action. Defaults to "throw". |
responsive | "media" | "container" | No | How breakpoints resolve: viewport @media (default) or @container against its own box. |
<JoyDom> renders into its own shadow root, so the document's global ids and classes stay isolated from the host page.
Rendering to a string
For non-component contexts — a server function, a test — render with react-dom/server:
const = (< ={} />);The markup contains a declarative shadow root (<template shadowrootmode>); mount it with element.setHTMLUnsafe(html) so the browser attaches the shadow.
Types
JoyDomProps
type JoyDomProps = JoyDomRenderOptions & {
spec: Spec;
/** Ref to the rendered content element inside the shadow root. */
ref?: Ref<HTMLDivElement>;
};JoyDomRenderOptions
The options <JoyDom> accepts alongside spec:
type JoyDomRenderOptions = {
components?: Record<string, JoyDomComponent>;
actions?: Record<string, ActionHandler>;
missingAction?: "ignore" | "throw";
responsive?: "media" | "container";
};JoyDomComponent<N>
A custom component:
type JoyDomComponent<N extends Node = Node> = ComponentType<JoyDomComponentProps<N>>;JoyDomComponentProps<N>
What custom components receive:
type JoyDomComponentProps<N extends Node = Node> = {
children?: ReactNode;
node: N;
alt?: string;
id?: string;
className?: string;
src?: string;
style?: CSSProperties;
};| Prop | What's in it |
|---|---|
children | Rendered child nodes and primitive values. |
node | The raw Joy DOM node. Useful when the type lets you read more than the renderer surfaces. |
id | Resolved node id (from props.id). |
className | Joined class string (renderer-generated + document classes). |
style | Resolved inline style for this platform. |
alt | img-only. The resolved alt attribute. |
src | img-only. The resolved src attribute. |
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;
};Error behavior
- A document fails
SpecSchemavalidation at the boundary you control.@joy-dom/reactdoes not callSpecSchema.parseitself, 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. - Style values that don't map to a CSS property are ignored. The Zod schema would have caught these if you validated upstream.
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, a guided walk through building a document.
- Custom components, the tutorial chapter on writing them.
@joy-dom/spec, the types this renderer consumes.- Specification, the supported nodes, properties, and breakpoints.