Originally published on the Grafloria engineering blog.
Look at the top-voted feature requests of any flow library and one theme towers over the rest: users want to put things inside other things. In React Flow's tracker it's "Nested flows", "Nested Nodes" and "absolute positioning in sub flows" — sixty-plus upvotes between them, one still open. This isn't a React Flow problem. It's that nesting looks like a styling feature and is actually a
semantics feature, and half-shipping it is worse than not shipping it.
Why it's genuinely hard
The moment a node can contain nodes, every subsystem gets a second opinion:
Dragging: does moving the parent move the children? (Yes.) Does dragging a
child near the edge leave the parent? (Your call — but it must be a call, not an accident. One of the top-voted react-flow Stack Overflow questions is literally "one node drags other nodes" — accidental containment.)Hit-testing: a click inside a container over empty space — who gets it?
The container? The canvas? The deepest nested thing under the pointer?Serialization: if membership isn't part of the document, a saved diagram
reloads as loose boxes over a decorative rectangle.Undo: dragging a node from one container into another touches two
containers and a membership record. One gesture must be one undo step.Collapse: fold a container and its links must re-anchor to the header —
and unfold must restore exactly what was there, even after a save/reload.
Libraries that treat groups as a rendering trick get the first demo right and the next four bullets wrong — which is exactly why the feature requests stay open for years. The correct-but-expensive answer is that containment has to live in the model.
Containment is membership
In Grafloria a group is a model object with members, and everything else follows from that one decision:
import { GroupModel } from '@grafloria/engine';
diagram.addGroup(new GroupModel({ id: 'ingest-stage', position: { x: 0, y: 0 },
size: { width: 320, height: 180 } }));
await engine.addToGroup('ingest-stage', 'a'); // membership is model state
await engine.collapseGroup('ingest-stage'); // reversible: a snapshot rides ON the group
await engine.expandGroup('ingest-stage'); // …so collapsed diagrams survive save/reload
Members drag with the group. Collapse stores member positions and removed links
on the group itself, so a collapsed diagram serializes and expands correctly in
a different session. Hit-testing is deepest-wins. And because group operations are commands, the cross-container drag really is one ⌘Z.
The dashboard version, since that's where nesting usually ends up
The most common concrete ask behind "nested flows" is a dashboard section: a box of KPIs that lays out its own children and accepts tiles dragged in. In our dashboard kit that's one field on a widget:
{ id: 'kpis', title: 'KPI section', span: 12, columns: 4,
widgets: [
{ id: 'k1', kind: 'kpi', span: 1, data: { label: 'Revenue', value: '$6.8M' } },
{ id: 'k2', kind: 'kpi', span: 1, data: { label: 'Customers', value: '1,284' } },
] }
Drag a tile across the boundary and the other grid adopts it live; drag it back out and the board takes it back; either way one undo restores everything, and
toJSON() reports the tile under whichever parent it's actually in —
derived from live membership, not from the array you authored. Resize a child taller than the section and the section grows a row in its board rather than letting the child overflow.
Try both live: group demos · dashboard containers · the concepts: Groups.
Grafloria is an MIT-licensed diagram engine for React, Angular, Vue and plain JavaScript — grafloria.com. If this post was useful, the demo gallery is where the ideas live as running code.
Top comments (0)