Element Palette
<Stencil /> is a headless drag source. You build the palette UI with React, then call useStencil().startCellDrag(...) on a pointer event to start a drag. The dropped cell is added to the linked <Paper /> automatically.
Reach for <Stencil /> whenever the user should populate the diagram by dragging from a side panel: shape libraries, BPMN element catalogs, custom block kits.
Building a palette
The palette below floats over the canvas and groups its items into two sections: tasks by status, and flow elements. Each tile is an ordinary React card wired to start a drag on pointerdown; drag one onto the paper to add a card. The preview that follows the pointer is the very same card, and it turns red while it sits over an invalid drop area (the palette panel, or off the paper).
Everything starts with useStencil(), available to any component inside <Stencil>: it returns { startCellDrag, cancelCellDrag, stencil }. Calling startCellDrag(template, event) on a tile's pointerdown is what turns a plain React card into a drag handle. Stop propagation on that event so the press doesn't also start a blank-area gesture on the paper underneath. The template is an ordinary CellRecord with no id or position (both are assigned when the cell lands), so a single piece of data describes both the palette tile and the element that drops onto the paper.
The palette itself is plain React. The headless stencil renders no UI of its own and has no concept of "sections," so grouping the tiles into tasks and flow elements (or arranging them any other way) is just markup; <Stencil> only wraps its children in a host <div> that className and style size and position like any other panel.
Because this first demo omits renderElement on <Stencil>, the drag preview reuses the paper's own renderer and looks exactly like the element it will become. To give the preview a different look, or to drop a different cell than you drag, see Customising the dropped shape.
Omit paper to use the single default paper. Pass a paper id or ref to target a specific paper when your app renders more than one. The stencil also pairs cleanly with <PaperScroller />: dropped cells land inside the scrollable viewport.
Reactive drag preview
The preview is rendered by React, so it can respond to the drag as it happens. Call useCellDrag() from inside the renderer to read the live drag state. The demo above uses canDrop to flag an invalid drop area and isPreview to apply that styling only to the in-flight clone, never the placed cards:
import { HTMLHost, useCellDrag } from '@joint/react-plus';
function Element({ label }: { label: string }) {
const { isPreview, canDrop } = useCellDrag();
const invalid = isPreview && !canDrop;
return <HTMLHost className={invalid ? 'border-red-500' : ''}>{label}</HTMLHost>;
}
useCellDrag() returns:
isDragging:truewhile a drag is in progress.isPreview:truefor the stencil drag preview,falsefor placed elements.canDrop:truewhile the pointer is over a valid drop area (the paper, outside the palette panel).dropArea: the drop bounding box (ag.Rect), while dragging.event/cellId: the latest pointer event and the dragged cell's id, while dragging.
Called outside an active drag it doesn't throw: isDragging is simply false.
useCellDrag() is the same @joint/react drag-state hook covered in the Event Handling chapter. Under a <Stencil>, its canDrop reflects whether the pointer is over a valid drop target.
Customising the dropped shape
By default the cell you drag is the cell that drops, rendered the same way throughout. Two independent props change that:
renderElementon<Stencil>gives the preview its own renderer. Omit it (as the first demo does) and the preview reuses the paper's renderer; pass it and the preview can be a wholly different React tree from the placed element.dropCell, in thestartCellDragoptions, lands a different cell than the one you drag. The dragged cell is only the preview;dropCellis the model actually added to the graph.
The demo below uses both. The palette drags a compact icon thumbnail while dropCell lands the full card:
startCellDrag(preview, event, { dropCell: card });
Because the preview and the dropped cell are separate records, they need not share a shape. The preview here carries no label, while the dropped card does, and each renderer is typed for exactly its own cell. Reach for dropCell when the palette shows small thumbnails but drops full-sized shapes, or when the dropped cell is resolved from a category at drop time.
Validating drops
A drop counts as valid whenever the pointer is over the paper and outside the palette panel. The stencil treats its own host element as a no-drop zone, which is what canDrop (and isValidDrop, in the events below) report. To reject a drop for your own reasons, pass an onCellDragEnd callback to startCellDrag and call cancelCellDrag(); the preview animates back to the palette and onCellDrop never fires:
startCellDrag(template, event, {
onCellDragEnd({ dropArea, isValidDrop }) {
if (!isValidDrop) return;
if (graph.findElementsInArea(dropArea).length > 0) {
cancelCellDrag();
}
},
});
graph.findElementsInArea(dropArea) returns existing elements that would overlap the drop. Reject in onCellDragEnd whenever you need a guard: overlapping a sibling, dropping outside an allowed region, or violating a custom rule.
Drag lifecycle
<Stencil> exposes five drag events. Pass them as props to listen across every drag, or in the third argument to startCellDrag to scope them to a single palette item:
| Event | Fires |
|---|---|
onCellDragStart | The drag has begun. |
onCellDrag | The pointer moved while dragging. |
onCellDragEnd | The drag is about to terminate. Call cancelCellDrag() here to reject the drop. |
onCellDrop | The dropped cell was added to the graph. |
onCellDropInvalid | The drag ended outside the paper or was cancelled. |
The in-progress callbacks (onCellDragStart, onCellDrag, onCellDragEnd) receive { model, event, dropArea, isValidDrop, stencil, paper, graph, dragPaper }. The terminal callbacks (onCellDrop, onCellDropInvalid) replace dropArea and isValidDrop with x and y, the drop point in paper-local coordinates.
Cancelling a drag
cancelCellDrag() aborts the in-flight drag from anywhere inside <Stencil> children. Wire it up for keyboard escape, context-menu interception, or any other gesture that should bail out:
import { useEffect } from 'react';
import { useStencil } from '@joint/react-plus';
function EscapeToCancel() {
const { cancelCellDrag } = useStencil();
useEffect(() => {
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') cancelCellDrag({ dropAnimation: false });
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [cancelCellDrag]);
return null;
}
Pass { dropAnimation: false } to skip the bounce-back animation; omit it to let the preview ease back to its origin.
Drag clone behaviour
Four boolean props on <Stencil> fine-tune the drag clone. All default to true; turn them off when the default feels wrong:
dropAnimation: animate the clone back to the palette on an invalid or cancelled drop. Setfalseto make rejection instant.scaleClones: scale the clone to match the paper's current zoom. Most visible with a zoomable paper such as<PaperScroller />.usePaperGrid: snap the clone to the target paper's grid while dragging.autoZIndex: lift the dragged clone above the rest of the diagram. Setfalsewhen your shapes use a custom z-stack you don't want disturbed.