NodeComponent
Renders each node as your own React component. It receives the node's data — including any custom fields you added — and can return any JSX: HTML, images, styled components, Tailwind classes, anything.
type NodeComponentType<N> = React.FC<{ node: N }>;
type Person = { id: number; name: string; role: string; color: string };
<Graph<Person>
graph={data}
NodeComponent={({ node }) => (
<div className="person-card" style={{ borderColor: node.color }}>
<strong>{node.name}</strong>
<small>{node.role}</small>
</div>
)}
/>;
Live — each card below is a plain <div> with Tailwind classes:
NodeComponent={PersonCard}Live
How it works
Each node is rendered inside an SVG <foreignObject> positioned by the simulation. That means:
- Regular HTML and CSS work — flexbox, borders, shadows, even other React components.
- The node sizes itself — width and height come from your component's natural size.
- Dragging is automatic — no event handlers needed (disable it with
isNodeDraggable).
If you don't pass a NodeComponent, a simple built-in white box is used.
Typed custom data
Graph is generic, so your node type flows into the component with full autocomplete:
import { Graph, type Node } from "d3-graph-react";
type Character = Node & { name: string; avatar: string };
const CharacterNode: React.FC<{ node: Character }> = ({ node }) => (
<figure className="character">
<img src={node.avatar} alt={node.name} width={48} height={48} />
<figcaption>{node.name}</figcaption>
</figure>
);
<Graph<Character> graph={data} NodeComponent={CharacterNode} />;
Keep nodes lightweight
NodeComponent renders once per node and re-renders on every simulation tick while the graph
is moving. Avoid heavy computation inside it — memoize expensive work with useMemo, or
pre-compute it in your data.