Use a group when a frame has meaning, not only appearance. The group owns its members, so a drag of the frame moves the members with it; groups can contain other groups; collapsing hides the members and expanding restores their saved geometry.
When to use it
Use this pattern for a pipeline, stage, swimlane, or any subflow that readers need to inspect as a unit. Membership is explicit rather than inferred from overlap: moving a member does not redraw the hierarchy. To change membership in code, call addToGroup() or removeFromGroup() on the engine.
Use render to mount the data and return a DiagramInstance; use instance.getModel() for the document and instance.getEngine() for group behavior.
Build the hierarchy
Create the outer group, add nodes to it, then create the inner group and add the inner group to the outer group. Fit the inner frame first and the outer frame second. The result is a Pipeline frame around the three stages, with a Retry handler frame around retry inside it. outside remains outside.
jsimport { render } from '@grafloria/element';
const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');
host.style.height = '100vh';
const instance = render({
nodes: [
{ id: 'n1', position: { x: 400, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 1' },
{ id: 'n2', position: { x: 560, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 2' },
{ id: 'n3', position: { x: 480, y: 275 }, size: { width: 120, height: 60 }, label: 'retry' },
{ id: 'outside', position: { x: 80, y: 150 }, size: { width: 120, height: 60 }, label: 'outside' },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n1', target: 'n3' },
],
}, host);
void (async () => {
const engine = instance.getEngine();
const diagram = instance.getModel();
const pipeline = await engine.addGroup({ name: 'Pipeline' });
await engine.addToGroup(pipeline.id, 'n1');
await engine.addToGroup(pipeline.id, 'n2');
await engine.addToGroup(pipeline.id, 'n3');
const retryHandler = await engine.addGroup({ name: 'Retry handler' });
await engine.addToGroup(retryHandler.id, 'n3');
pipeline.addMember(retryHandler.id, diagram);
retryHandler.fitToContents(diagram);
pipeline.fitToContents(diagram);
instance.renderNow();
// The group frame is draggable, and its members travel with it.
await engine.collapseGroup(pipeline.id);
instance.renderNow();
await engine.expandGroup(pipeline.id);
instance.renderNow();
})();
tsimport { AfterViewInit, Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: '<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges" style="display:block;height:100vh" />',
})
export class SubflowComponent implements AfterViewInit {
readonly canvas = viewChild.required(DiagramCanvasComponent);
readonly nodes = [
{ id: 'n1', position: { x: 400, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 1' },
{ id: 'n2', position: { x: 560, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 2' },
{ id: 'n3', position: { x: 480, y: 275 }, size: { width: 120, height: 60 }, label: 'retry' },
{ id: 'outside', position: { x: 80, y: 150 }, size: { width: 120, height: 60 }, label: 'outside' },
];
readonly edges = [
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n1', target: 'n3' },
];
async ngAfterViewInit(): Promise<void> {
const engine = this.canvas().activeEngine();
if (!engine) return;
const diagram = engine.getDiagram();
if (!diagram) return;
const pipeline = await engine.addGroup({ name: 'Pipeline' });
await engine.addToGroup(pipeline.id, 'n1');
await engine.addToGroup(pipeline.id, 'n2');
await engine.addToGroup(pipeline.id, 'n3');
const retryHandler = await engine.addGroup({ name: 'Retry handler' });
await engine.addToGroup(retryHandler.id, 'n3');
pipeline.addMember(retryHandler.id, diagram);
retryHandler.fitToContents(diagram);
pipeline.fitToContents(diagram);
}
}
tsximport { component$ } from '@builder.io/qwik';
const nodes = [
{ id: 'n1', position: { x: 400, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 1' },
{ id: 'n2', position: { x: 560, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 2' },
{ id: 'n3', position: { x: 480, y: 275 }, size: { width: 120, height: 60 }, label: 'retry' },
{ id: 'outside', position: { x: 80, y: 150 }, size: { width: 120, height: 60 }, label: 'outside' },
];
export default component$(() => (
<div style={{ height: '100vh' }}>
<grafloria-flow data-nodes={JSON.stringify(nodes)} data-groups="Pipeline: n1,n2,n3; Retry handler: n3" />
</div>
));
vue<script setup lang="ts"> import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/vue'; const nodes = [ { id: 'n1', position: { x: 400, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 1' }, { id: 'n2', position: { x: 560, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 2' }, { id: 'n3', position: { x: 480, y: 275 }, size: { width: 120, height: 60 }, label: 'retry' }, { id: 'outside', position: { x: 80, y: 150 }, size: { width: 120, height: 60 }, label: 'outside' }, ]; const edges = [ { id: 'e1', source: 'n1', target: 'n2' }, { id: 'e2', source: 'n1', target: 'n3' }, ]; async function onInit(instance: DiagramInstance): Promise<void> { const engine = instance.getEngine(); const diagram = engine.getDiagram(); if (!diagram) return; const pipeline = await engine.addGroup({ name: 'Pipeline' }); await engine.addToGroup(pipeline.id, 'n1'); await engine.addToGroup(pipeline.id, 'n2'); await engine.addToGroup(pipeline.id, 'n3'); const retryHandler = await engine.addGroup({ name: 'Retry handler' }); await engine.addToGroup(retryHandler.id, 'n3'); pipeline.addMember(retryHandler.id, diagram); retryHandler.fitToContents(diagram); pipeline.fitToContents(diagram); } </script> <template> <div style="height:100vh"><GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" /></div> </template>
tsximport { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/react';
const nodes = [
{ id: 'n1', position: { x: 400, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 1' },
{ id: 'n2', position: { x: 560, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 2' },
{ id: 'n3', position: { x: 480, y: 275 }, size: { width: 120, height: 60 }, label: 'retry' },
{ id: 'outside', position: { x: 80, y: 150 }, size: { width: 120, height: 60 }, label: 'outside' },
];
const edges = [
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n1', target: 'n3' },
];
export default function Subflow() {
const onInit = (instance: DiagramInstance): void => {
const engine = instance.getEngine();
const diagram = engine.getDiagram();
if (!diagram) return;
void (async () => {
const pipeline = await engine.addGroup({ name: 'Pipeline' });
await engine.addToGroup(pipeline.id, 'n1');
await engine.addToGroup(pipeline.id, 'n2');
await engine.addToGroup(pipeline.id, 'n3');
const retryHandler = await engine.addGroup({ name: 'Retry handler' });
await engine.addToGroup(retryHandler.id, 'n3');
pipeline.addMember(retryHandler.id, diagram);
retryHandler.fitToContents(diagram);
pipeline.fitToContents(diagram);
instance.renderNow();
})();
};
return <div style={{ height: '100vh' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit={onInit} /></div>;
}
Collapse and restore the subflow
Call collapseGroup(groupId, options?) on the engine. The members disappear, a collapsed placeholder represents the group, and links crossing the boundary point to that placeholder; parallel boundary links can appear as one aggregated proxy link. Call expandGroup(groupId) to restore the members, links, and the geometry captured before collapse.
The proxyLabel option controls the text supplied for an aggregated proxy link:
| Option | Type | Default | What it does |
|---|---|---|---|
proxyLabel | (info: { count: number }) => string | not specified | Returns the label for an aggregated proxy link. |
For a visible control, put the two engine calls behind buttons rather than collapsing immediately after setup. This complete browser example renders the group and wires both buttons:
jsimport { render } from '@grafloria/element';
const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');
host.style.height = '100vh';
const collapseButton = document.createElement('button');
collapseButton.textContent = 'Collapse group';
const expandButton = document.createElement('button');
expandButton.textContent = 'Expand group';
document.body.prepend(expandButton);
document.body.prepend(collapseButton);
const instance = render({
nodes: [
{ id: 'outside', position: { x: 80, y: 150 }, size: { width: 120, height: 60 }, label: 'outside' },
{ id: 'member', position: { x: 400, y: 150 }, size: { width: 120, height: 60 }, label: 'member' },
],
edges: [{ id: 'link', source: 'outside', target: 'member' }],
}, host);
void (async () => {
const engine = instance.getEngine();
const pipeline = await engine.addGroup({ name: 'Pipeline' });
pipeline.setFrame({ x: 360, y: 100, width: 220, height: 160 });
await engine.addToGroup(pipeline.id, 'member');
instance.renderNow();
collapseButton.addEventListener('click', async () => {
await engine.collapseGroup(pipeline.id, { proxyLabel: (info) => `${info.count}×` });
instance.renderNow();
});
expandButton.addEventListener('click', async () => {
await engine.expandGroup(pipeline.id);
instance.renderNow();
});
})();
After collapse(), the canvas shows the collapsed group placeholder instead of its stages. After expand(), the stages and their pre-collapse positions return. Drag the group frame between those calls to move the subflow as a unit.
Pitfalls
- A group is not a decorative rectangle: adding a member records containment, so moving the group moves its members.
- Fit nested groups from the inside out.
fitToContents()uses the current member geometry, so fit the child before the parent. - Keep the diagram host at a real height; a canvas whose host has no resolved height renders blank. See Edit Mermaid diagrams.
- Collapse after the instance is initialized and repaint with
renderNow()when the next line needs the new pixels. See The DiagramInstance.
Live demo
Try the sub-flow demo, then the collapse and expand demo. For compound layout across nested containers, see the nested containers demo.
Was this page helpful?