Batch Switch CSS Line Styles
This example keeps a small relation-graph tree fixed and batch-switches every rendered edge among six CSS class families at runtime. It combines per-line `className` metadata, SCSS overrides on built-in line layers, icon-slot nodes, shared canvas controls, and image export without replacing the native line renderer.
Batch-Switching CSS Line Styles on a Relation Graph Tree
What This Example Builds
This example builds a small left-to-right tree viewer whose main purpose is comparing relation-graph line skins. The canvas shows circular icon nodes, a green gradient background, and nine links that start with different className values, label modes, and curve settings.
Users can open a floating control window and switch all current lines to one of six predefined CSS families. The visible result is not just a stroke change. The line halo, dash pattern, detached label chip, and text-on-path color all change together, while the graph structure stays fixed.
The most useful idea here is that the example keeps relation-graph’s built-in line renderer and still achieves strong visual variation through CSS classes and runtime updateLine() calls.
How the Data Is Organized
The data is declared inline in initializeGraph() as one RGJsonData object with rootId, a flat nodes array, and a flat lines array. Each node stores an icon key in node.data.icon, and each line can carry its own presentation metadata such as className, useTextOnPath, lineShape, fromJunctionPoint, and toJunctionPoint.
There is one preprocessing step before setJsonData(): the code rewrites every line label to className=..., so the graph immediately shows which class family each edge is using. After that, the JSON is loaded directly with no external fetch, no layout-time transformation, and no structural editing step.
In a real system, the same shape could represent a feature tree, service dependency map, workflow family, or product taxonomy. node.data.icon can stand for category or status, while the per-line metadata can represent relationship types, risk levels, transport modes, or other edge categories that need consistent CSS-based styling.
How relation-graph Is Used
index.tsx wraps the example in RGProvider, so both the graph and the shared utility window resolve the same graph context. Inside MyGraph.tsx, RelationGraph is rendered with a tree layout that grows from left to right and uses wide spacing (treeNodeGapH: 310, treeNodeGapV: 70) so the icon nodes and label variations remain easy to compare.
The graph options establish a neutral base that CSS can restyle. Nodes are circular, transparent by default, outlined in white, and checked items receive a translucent white background token. Lines default to translucent white and straight segments, while a subset of records overrides itself to RGLineShape.StandardCurve with left-right junction points.
The example uses RGHooks.useGraphInstance() to load the dataset, center and fit the viewport, read the current rendered lines, and update each line after a style switch. RGSlotOnNode replaces the default node body with a Lucide icon chosen from node.data.icon. The example does not use a custom line slot. Instead, my-relation-graph.scss targets built-in relation-graph elements such as .rg-line-peel, .rg-line-bg, .rg-line, .rg-line-label, and .rg-line-text.
The floating helper UI comes from the shared DraggableWindow component. Its settings overlay uses RGHooks.useGraphStore() to reflect the current wheel and drag modes, and setOptions(...) to change them at runtime. The same helper also exposes image export through prepareForImageGeneration(), getOptions(), and restoreAfterImageGeneration().
Key Interactions
The central interaction is the six-style selector in the floating window. Clicking one option updates local state, iterates graphInstance.getLines(), and rewrites every current line’s className and visible text. The whole tree switches to one visual family in one pass.
The floating window itself is interactive. Users can drag it, minimize it, and reopen it without affecting the graph state.
The settings overlay adds two viewer-level controls: switching the wheel between scroll, zoom, and none, and switching canvas dragging between selection, move, and none. It also provides a download action that captures the current graph view as an image.
Key Code Fragments
This options block shows that layout and core geometry stay inside relation-graph while the visible styling is meant to be overridden from CSS.
const graphOptions: RGOptions = {
defaultLineColor: 'rgba(255, 255, 255, 0.6)',
defaultNodeColor: 'transparent',
defaultNodeBorderWidth: 1,
defaultNodeBorderColor: '#fff',
checkedItemBackgroundColor: 'rgba(255,255,255,0.3)',
defaultNodeShape: RGNodeShape.circle,
toolBarDirection: 'h',
toolBarPositionH: 'right',
toolBarPositionV: 'bottom',
defaultLineShape: RGLineShape.StandardStraight,
layout: {
layoutName: 'tree',
from: 'left',
treeNodeGapH: 310,
treeNodeGapV: 70
}
};
This data fragment shows that line presentation metadata is stored directly on each record before the graph is loaded.
const myJsonData: RGJsonData = {
rootId: 'a',
nodes: [
{ id: 'a', text: 'a', data: { icon: 'align_bottom' } },
{ id: 'b', text: 'b', data: { icon: 'basketball' } },
// ...
],
lines: [
{ id: 'line-1', from: 'a', to: 'b', text: 'Relation description', className: 'my-line-class-01' },
{ id: 'line-3', from: 'a', to: 'b2', useTextOnPath: true, text: 'Relation description', className: 'my-line-class-02' },
{ id: 'line-4', from: 'b2', to: 'b2-1', lineShape: RGLineShape.StandardCurve, fromJunctionPoint: RGJunctionPoint.lr, toJunctionPoint: RGJunctionPoint.lr, text: 'Relation description', className: 'my-line-class-02' }
]
};
This preprocessing step proves that the example turns the line label itself into a live reference for the active class name.
myJsonData.lines.forEach(line => {
line.text = `className=${line.className || ''}`;
});
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();
This update function is the core runtime behavior: it batch-rewrites every rendered line to the selected CSS family instead of rebuilding the JSON.
const changeAllLineClassName = (newClassName: string) => {
setLineStyle(newClassName);
const allLines = graphInstance.getLines();
allLines.forEach(line => {
graphInstance.updateLine(line.id, {
className: `my-line-class-${newClassName}`,
text: `className=${newClassName}`
});
});
}
This render fragment shows that the example combines a floating selector with a custom node slot, while leaving line rendering to relation-graph itself.
<DraggableWindow width={500}>
<div className="pb-4 text-base">Define line style via CSS</div>
<div className="pb-2">Change the className property of all lines in batch:</div>
<SimpleUISelect
data={[
{ value: '01', text: 'Style 01' },
{ value: '02', text: 'Style 02' },
{ value: '03', text: 'Style 03' }
]}
onChange={(newValue: string) => changeAllLineClassName(newValue)}
currentValue={lineStyle}
small={true}
/>
</DraggableWindow>
<RelationGraph options={graphOptions}>
<RGSlotOnNode>{/* icon node renderer */}</RGSlotOnNode>
</RelationGraph>
This SCSS fragment shows how one class family restyles both the line layers and the two label modes provided by the built-in renderer.
.rg-line-peel.my-line-class-01 {
.rg-line-bg {
stroke: rgba(244, 60, 229, 0.68);
stroke-width: calc(var(--rg-line-width) + 6px);
stroke-dasharray: 20, 20, 20;
opacity: 1;
}
.rg-line-label {
color: rgba(244, 60, 229, 1);
background-color: rgb(205, 204, 204);
}
.rg-line-text {
fill: rgba(244, 60, 229, 1);
}
}
This shared settings code shows that canvas behavior and export are adjusted through graph-instance APIs rather than separate page state.
const downloadImage = async () => {
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
if (imageBlob) {
downloadBlob(imageBlob, 'my-image-name');
}
await graphInstance.restoreAfterImageGeneration();
};
What Makes This Example Distinct
The comparison data places this example closest to custom-line-animation, line-style1, line-style2, customer-line1, and ever-changing-tree, but its emphasis is narrower and more CSS-centered than those neighbors. Its strongest distinguishing point is that it keeps relation-graph’s native line renderer, assigns per-line className metadata, and then batch-switches every rendered edge among six SCSS families from one floating selector.
Compared with custom-line-animation, this example is less about motion presets or filter effects and more about static CSS skinning of built-in strokes, label chips, and path text. Compared with line-style1 and line-style2, it goes beyond fixed line properties or checked-line emphasis by rewriting every current line’s class at runtime.
Compared with customer-line1, the important difference is architectural: this example does not replace edges with RGSlotOnLine geometry. It shows how far CSS can go while keeping the stock line renderer. The comparison data also notes that it is unusual within this cluster to use the same class system across both detached labels and useTextOnPath labels in one graph.
Where Else This Pattern Applies
This pattern applies to products where edge appearance should be owned by a design system rather than by custom SVG line rendering. Examples include workflow viewers, product dependency maps, service relationship dashboards, and taxonomy graphs where relationship categories need visually consistent CSS families.
It is also useful when one line-style mechanism has to cover different label modes at the same time. Teams can adapt the same approach when they need detached chips for some edges, text on path for others, and a runtime switcher for demos, white-label themes, training tools, or stakeholder review screens.