Slot-Based Node Content and Styles
This example renders a left-to-right tree whose nodes are replaced with custom React slot content instead of the default node body. One static dataset mixes a carousel card, a video card, button nodes, circular badges, and glass-style labels, while a shared floating panel adds runtime canvas settings and image export.
Slot-Based Node Content and Styles in a Tree Graph
What This Example Builds
This example builds a left-to-right tree graph whose nodes do not share one visual template. Instead, the graph uses a single node slot to render several distinct node bodies: a large root card with a carousel, a video card, rectangular button nodes, circular badges, and translucent pill labels.
The visible result is a full-height presentation canvas with a lime-green background, semi-transparent white connectors, and a floating white utility window. Users can inspect the graph as a viewer, interact with widgets embedded inside nodes, open the floating settings panel, switch canvas interaction modes, and export the current graph as an image.
The most important point is not the tree itself. The example shows how one static relation-graph dataset can drive heterogeneous React node content through RGSlotOnNode, while SCSS redirects the checked-state styling onto the rendered slot content instead of the library’s default node body.
How the Data Is Organized
The graph data is created inline as one RGJsonData object with rootId, nodes, and lines. There is no fetch, no async transformation pipeline, and no dynamic graph editing after load. The structure is simple:
nodeshold the graph identity and presentation hints.linesdefine the tree relationships.typedecides which custom node bodyMyNodeContentshould render.data.buttonTextcarries per-node payload for button labels.nodeShapestill matters for relation-graph geometry, even though the visible body is mostly custom slot content.
There is no separate preprocessing step before setJsonData(). The example tags the nodes directly while assembling the JSON literal, so the slot renderer can branch on metadata immediately after the graph loads.
In a real application, the same pattern could map to organization roles, workflow states, media-rich content cards, KPI badges, alert nodes, or action nodes where each node type needs different HTML structure but still belongs to one graph dataset.
How relation-graph Is Used
The graph uses relation-graph as a viewer-oriented tree layout. MyGraph configures a tree layout that grows from left to right with wide horizontal spacing and tighter vertical spacing, which keeps the mixed node bodies readable. The options also remove most of the default node chrome by setting transparent node fills and a zero-width default border, letting the slot-rendered React content define the look.
RelationGraph is wrapped by RGProvider, and the demo reads the live graph instance through RGHooks.useGraphInstance(). That instance is used for the one-time initialization flow: load data with setJsonData(...), move the viewport to center, and then call zoomToFit(). The same hook is reused inside the shared floating helper to update interaction options and run the image-export pipeline.
The main customization point is RGSlotOnNode. Every node passes through one slot function, which forwards the current node object into MyNodeContent. That component branches by node.type and renders:
- a carousel card for the root node,
- a video card with built-in media controls,
- black action buttons for
my-button, - translucent circular badges for
my-circle, - a glass-style pill label for the remaining nodes.
SCSS completes the customization. The stylesheet gives the toolbar a translucent white appearance, removes the default checked-node shadow, and applies the highlight ring to the custom slot wrapper instead. That matters because the visible node body is no longer the default relation-graph box.
There is no graph editing flow here. The example is firmly in viewer mode, with runtime option changes limited to canvas interaction behavior and export support.
Key Interactions
- The root node contains a carousel with Previous and Next controls, proving that interactive React widgets can live inside a node body.
- The video node exposes native media controls, showing that slot content can include rich HTML media rather than only text or icons.
- The button nodes handle DOM clicks inside node content and log the current node id. This is a small behavior, but it demonstrates that custom node widgets can own their own event handling.
- The floating utility window can be dragged, minimized, and switched into a settings overlay.
- The settings overlay changes
wheelEventActionanddragEventActionon the live graph instance, so users can switch between scroll, zoom, selection, move, or no canvas response without reloading data. - The same overlay can export the graph to an image by preparing the graph DOM, capturing it with
modern-screenshot, downloading the blob, and then restoring the graph state.
Key Code Fragments
This fragment shows that the example depends on relation-graph layout and option tuning to frame custom node content on a presentation-style canvas.
const graphOptions: RGOptions = {
debug: true,
backgroundColor: 'rgb(101, 163, 13)',
defaultLineColor: 'rgba(255, 255, 255, 0.6)',
defaultNodeColor: 'transparent',
defaultNodeBorderWidth: 0,
defaultNodeShape: RGNodeShape.rect,
toolBarDirection: 'h',
toolBarPositionH: 'right',
toolBarPositionV: 'bottom',
defaultPolyLineRadius: 10,
defaultLineShape: RGLineShape.StandardCurve,
defaultJunctionPoint: RGJunctionPoint.lr,
layout: {
layoutName: 'tree',
from: 'left',
treeNodeGapH: 200,
treeNodeGapV: 30
}
};
This fragment shows that the graph data itself carries the node-type metadata used by the slot renderer.
const myJsonData: RGJsonData = {
rootId: 'a',
nodes: [
{ id: 'a', text: 'a', type: 'my-root', nodeShape: RGNodeShape.rect },
{ id: 'b', text: 'b', type: 'my-circle', nodeShape: RGNodeShape.circle },
// ...
{ id: 'c', text: 'c', type: 'my-video' },
{ id: 'c1', text: 'c1', type: 'my-button', nodeShape: RGNodeShape.rect, data: { buttonText: 'Button 1' } },
{ id: 'c2', text: 'c2', type: 'my-button', nodeShape: RGNodeShape.rect, data: { buttonText: 'Button 2' } }
],
This fragment shows the core slot-rendering technique: one node component branches into multiple node-body implementations.
if (node.type === 'my-root') {
return (
<div className="bg-white rounded">
<SimpleUICarousel contents={nodeContentTypes} width={400} height={200} />
</div>
);
} else if (node.type === 'my-video') {
return (
<div className="relative h-56 w-72 rounded-lg overflow-hidden">
<video playsInline className="h-full w-full object-cover overflow-clip-margin-content-box overflow-clip" controls autoPlay loop muted src="https://relation-graph.com/images/video-dribbble.mp4" />
This fragment shows how the stylesheet moves the checked-state emphasis from the default library node box onto the custom slot content.
.rg-node-peel.rg-node-checked {
.rg-node {
box-shadow: none;
& > div {
box-shadow: 0 0 0 8px var(--rg-checked-item-bg-color);
}
}
}
This fragment shows that the shared helper changes live graph interaction modes and supports image export through graph-instance APIs.
<SettingRow
label="Wheel Event:"
options={[
{ label: 'Scroll', value: 'scroll' },
{ label: 'Zoom', value: 'zoom' },
{ label: 'None', value: 'none' },
]}
value={wheelMode}
onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>
What Makes This Example Distinct
Compared with nearby examples such as node-style2, node-style3, and node-style4, this one is most useful when the requirement is heterogeneous node-body replacement rather than theme refinement. The comparison data is clear on that point: other examples may also use RGSlotOnNode, but they usually apply one repeated visual system or focus on broader style comparison. This example uses one compact tree to place several visibly different content types into nodes at the same time.
The strongest distinguishing combination is:
- a left-to-right tree layout,
- transparent default node bodies,
- semi-transparent white curved connectors,
- a lime-green full-height canvas,
- multiple
node.type-driven slot bodies, - checked-state styling that wraps the custom slot content,
- a reused floating helper for canvas settings and export.
That combination makes it a stronger starting point than node-style2 when the built-in node renderer is not enough, and a more focused reference than node when the goal is not broad technique comparison but one practical pattern for mixed HTML and media inside nodes.
The floating helper window should be treated as secondary. The comparison data explicitly warns against presenting it as unique, because that shell is reused across nearby examples. The distinctive lesson here is the slot-based node content system and the styling adjustments required to make selection and chrome still look coherent.
Where Else This Pattern Applies
This pattern transfers well to scenarios where one graph must mix several node presentation modes without changing the graph data contract:
- product or content maps where some nodes are labels, some are media previews, and some are action cards,
- workflow dashboards where review nodes, decision buttons, and status badges need different HTML bodies,
- educational or storytelling graphs where root nodes act as feature cards and child nodes act as compact tags or checkpoints,
- monitoring views where alert nodes, metric pills, and drill-down actions share the same topology but not the same rendering needs.
The same structure can also be extended by replacing the inline sample data with API data, expanding node.type into a richer rendering registry, or connecting embedded node controls to real business actions instead of console logging.