Joy DOM

Server rendering

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

<Joy> renders into its own shadow root. On the server that is a declarative shadow root (<template shadowrootmode>). The browser attaches it while parsing the HTML, which means React cannot hydrate that subtree afterwards. Two approaches work around this.

Static or string rendering

renderToStaticMarkup(<Joy spec={spec} />) produces the declarative shadow root. Mount it with element.setHTMLUnsafe(html) so the browser attaches it. No hydration is involved, which suits static generation or a server function that returns markup.

Inside a hydrated app

For Next.js, Waku, or Remix, render <Joy> on the client so its shadow root attaches client-side rather than hydrating. Gate it on a mounted flag:

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return mounted ? <Joy spec={spec} /> : null;

Joy DOM's own website does exactly this for its previews.

Rendering to a string

For non-component contexts such as a server function or 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 is not part of your build, fetch it and validate at the boundary:

import { ,  } from "react";
import {  } from "@joy-dom/react";
import { , type  } from "@joy-dom/core";

export function ({  }: { : string }) {
  const [, ] = < | null>(null);

  (() => {
    ()
      .(() => .())
      .(() => .())
      .();
  }, []);

  if (!) return null;
  return < ={} />;
}

SpecSchema.parse at the boundary makes a malformed document fail fast with a readable Zod error.

Integration patterns

Document sourceApproach
Bundled with the appImport the JSON directly (import card from "./card.json"). Bundlers tree-shake to the documents you reference.
Served from an APIFetch and validate with SpecSchema, as above.
Generated per userRender on the server and pass to the client as initial props. Validate on the server boundary, not the client.

On this page