Joy DOM

Server rendering

Shadow-root tradeoffs, rendering to a string, and loading documents at runtime.

<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.

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.

Load documents at runtime

When the document isn't part of your build, fetch it and validate at the boundary:

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.

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.

API-served documents. Fetch and validate via 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).

On this page