> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withheadlight.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed with React

> Host the Headlight household chart from a React component. The chart still runs in a cross-origin iframe.

The chart still loads from `embed.withheadlight.com`. React only hosts the iframe and talks to it with `postMessage`. Headlight servers never receive names, trusts, or relationship data.

## Component

Copy this component. It targets React 19. On React 18, keep the latest `token` and `data` in refs instead of `useEffectEvent`. Mint the token on your server, then pass it in as a prop.

```tsx theme={null}
"use client";

import { useEffect, useEffectEvent, useRef } from "react";

const EMBED_ORIGIN = "https://embed.withheadlight.com";
const VERSION = "0.1.0";

type HeadlightChartProps = {
  token: string;
  data: unknown;
  embedOrigin?: string;
  version?: string;
  height?: number;
  onNodeClick?: (nodeId: string, nodeType: string) => void;
  onError?: (error: { code: string; message: string }) => void;
};

export function HeadlightChart({
  token,
  data,
  embedOrigin = EMBED_ORIGIN,
  version = VERSION,
  height = 720,
  onNodeClick,
  onError,
}: HeadlightChartProps) {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const readyRef = useRef(false);

  const src = `${embedOrigin.replace(/\/$/, "")}/chart/v${version}/index.html`;
  const chartOrigin = new URL(src).origin;

  const postRender = useEffectEvent(() => {
    iframeRef.current?.contentWindow?.postMessage(
      { type: "render", v: 1, token, payload: data },
      chartOrigin
    );
  });

  const onMessage = useEffectEvent((event: MessageEvent) => {
    const iframe = iframeRef.current;
    if (event.origin !== chartOrigin || event.source !== iframe?.contentWindow) {
      return;
    }
    if (typeof event.data !== "object" || event.data == null) {
      return;
    }

    if (event.data.type === "ready") {
      readyRef.current = true;
      postRender();
      return;
    }
    if (event.data.type === "nodeClick") {
      onNodeClick?.(event.data.nodeId, event.data.nodeType);
      return;
    }
    if (event.data.type === "error") {
      onError?.({ code: event.data.code, message: event.data.message });
    }
  });

  useEffect(() => {
    const iframe = iframeRef.current;
    if (iframe == null) {
      return;
    }

    const handleMessage = (event: MessageEvent) => {
      onMessage(event);
    };

    window.addEventListener("message", handleMessage);
    iframe.src = src;
    return () => {
      readyRef.current = false;
      window.removeEventListener("message", handleMessage);
      iframe.removeAttribute("src");
    };
  }, [chartOrigin, src]);

  useEffect(() => {
    if (!readyRef.current) {
      return;
    }
    postRender();
  }, [token, data]);

  return (
    <iframe
      ref={iframeRef}
      title="Headlight household chart"
      sandbox="allow-scripts allow-same-origin"
      style={{ width: "100%", height, border: 0 }}
    />
  );
}
```

Pin an immutable version prefix (`/chart/v0.1.0/index.html`). Do not load `/chart/latest`.

## Usage

Use the [Calderwood sample payload](/embed/sample-payload) as `chartData`.

```tsx theme={null}
<HeadlightChart
  token={token}
  data={chartData}
  version="0.1.0"
  onNodeClick={(nodeId, nodeType) => {
    console.log(nodeId, nodeType);
  }}
/>
```

`frameOrigin` on the mint request must be the origin of the page that hosts this component (bare origin, no trailing slash).

## Token

Mint from your server with an API key that has `embed:chart`. Pass the JWT into the component. Do not call the mint route from the browser or ship that API key to the client.

## Updates and commands

Changing `token` or `data` posts another `render`. That replaces the graph.

To search or reset the view after `ready`, post to the iframe the same way the component posts `render`:

```ts theme={null}
iframe.contentWindow.postMessage({ type: "find", v: 1, query: "Chen" }, chartOrigin);
iframe.contentWindow.postMessage({ type: "resetView", v: 1 }, chartOrigin);
```

See the [postMessage protocol](/embed/postmessage-protocol) for `focus` and `exclude`.

## Sandbox

Use exactly `sandbox="allow-scripts allow-same-origin"`. Do not add `allow-top-navigation` or `allow-popups`.

## What not to do

Always pass the embed CDN origin as `postMessage` `targetOrigin`. Do not use `*`.
