Containers & Grouping
In @joint/react, containers and grouping are built on top of JointJS embedding.
The hierarchy lives in graph state through the parent field on elements and links. Once a cell is embedded, it moves together with its parent and participates in the same local hierarchy.
This chapter covers embedding and grouping only. Collapse and expand behavior builds on the same ideas and belongs in a separate chapter.
The editor below has two containers, each with one embedded child, and one orphan node. Drag the orphan into either container to embed it; the sidebar shows the live hierarchy. Drag a child between containers to re-parent it, or drag it out to unembed it entirely. The blue highlight appears when hovering over a valid parent during drag.
The important pieces are:
- the
parentfield stores the hierarchy in graph state embeddingModelets users drag cells in and out of containersvalidateEmbedding()restricts which cells are allowed to become parentshighlighting.embeddinggives visual feedback while dragging over a valid parent
Declarative vs drag-and-drop embedding​
You can use embedding in two ways:
- Declarative embedding: set
parentdirectly on the element or link record - Drag-and-drop embedding: turn on
embeddingModeand let users create or remove the relationship by dragging
Declarative parent works even when embeddingMode is disabled. Use it when your hierarchy comes from saved data or application logic. In the example above, child-1 and child-2 start with declarative parent set to one of the containers, while the orphan node demonstrates drag-and-drop embedding through embeddingMode.
embeddingMode adds the editor behavior on top. When a user drags a cell into another valid cell, JointJS updates the parent relationship automatically. Dragging it back out unembeds it again.
Controlling valid parents​
validateEmbedding() is the main control point. It decides whether the dragged cell is allowed to become a child of the current potential parent.
<Paper
embeddingMode
validateEmbedding={({ child, parent }) => {
if (child.model.get('data')?.isContainer) return false;
return Boolean(parent.model.get('data')?.isContainer);
}}
/>
Other Paper options can refine the behavior (see Component options for the full passthrough):
frontParentOnlycontrols whether only the frontmost candidate is considered or whether JointJS should keep checking lower candidatesfindParentBycustomizes how the potential parent is chosen while dragging. It is reachable via theoptionsescape hatch on<Paper>:<Paper options={{ findParentBy: 'pointer' }} />
For many apps, embeddingMode plus validateEmbedding() is enough.
Locking children to their container​
By default, drag-and-drop embedding lets users pull a child cell out of its parent as easily as putting it in. Return false from validateUnembedding to keep specific cells locked inside their container:
<Paper
embeddingMode
validateUnembedding={({ child }) => {
return !child.model.get('data')?.locked;
}}
/>
validateUnembedding receives { child, paper, graph }. There is no parent, since the cell is leaving its current parent rather than entering a new one.
Embedding highlight​
When embeddingMode is on, the paper highlights the potential parent while the user drags a child over it. The highlighting.embedding option customizes the visual feedback. The demo uses the mask highlighter with a blue stroke:
<Paper
embeddingMode
highlighting={{
embedding: {
name: 'mask',
options: {
padding: 5,
attrs: {
stroke: '#3b82f6',
strokeWidth: 2,
strokeLinejoin: 'round',
},
},
},
}}
/>
highlighting.embedding accepts any of the built-in highlighters (mask, stroke, opacity, addClass, or list), each with its own option set.
Without this option, the paper still highlights potential parents using the default highlighter.
Container sizing note​
Embedding only defines the hierarchy. If a container should resize around its children, call native JointJS element methods such as fitToChildren() or fitParent() through useGraph() or event handlers.
That is useful for container-style editors, but it is a native JointJS sizing helper rather than a dedicated React abstraction. See Querying the Graph for the general pattern of reaching into the native graph from React.
Links and hierarchy​
Links can also participate in the same hierarchy by setting parent on a link cell:
{
id: 'link-1',
type: 'link',
source: { id: 'child-a' },
target: { id: 'child-b' },
parent: 'container-id',
}
That keeps the link in the same local context as its embedded endpoints. When the container moves, the embedded link moves with it.