Skip to main content
Version: 4.3

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.

Targeting a specific paper

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: true while a drag is in progress.
  • isPreview: true for the stencil drag preview, false for placed elements.
  • canDrop: true while the pointer is over a valid drop area (the paper, outside the palette panel).
  • dropArea: the drop bounding box (a g.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.

info

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:

  • renderElement on <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 the startCellDrag options, lands a different cell than the one you drag. The dragged cell is only the preview; dropCell is 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:

EventFires
onCellDragStartThe drag has begun.
onCellDragThe pointer moved while dragging.
onCellDragEndThe drag is about to terminate. Call cancelCellDrag() here to reject the drop.
onCellDropThe dropped cell was added to the graph.
onCellDropInvalidThe 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. Set false to 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. Set false when your shapes use a custom z-stack you don't want disturbed.