Skip to main content
Version: 4.3

Selecting Elements

Selection is the backbone of an editor: click a node to select it, see it highlight, act on it. In JointJS+ this is a built-in feature, so you don't wire pointer events or hold selection in React state yourself.

import {
  Diagram,
  Paper,
  PaperScroller,
  Selection,
  HTMLHost,
  useIsCellSelected,
  useSelection,
  useCells,
  linkRoutingOrthogonal,
  type ElementPort,
} from '@joint/react-plus';
import '@joint/react-plus/styles.css';
import './example.css';

type NodeData = { label: string; description: string; icon: string; color: string };

const ports: Record<string, ElementPort> = {
  in: { cx: 0, cy: '50%', width: 10, height: 10, passive: true },
  out: { cx: '100%', cy: '50%', width: 10, height: 10 },
};

const node = (id: string, x: number, y: number, data: NodeData) => ({
  id,
  type: 'element' as const,
  position: { x, y },
  size: { width: 160, height: 52 },
  portMap: ports,
  data,
});

const initialCells = [
  node('a', 60, 50, { label: 'New Ticket', description: 'Trigger', icon: 'âš¡', color: '#f59e0b' }),
  node('b', 320, 50, { label: 'Is Urgent?', description: 'Condition', icon: 'â—†', color: '#a855f7' }),
  node('c', 320, 180, { label: 'Send Reply', description: 'Action', icon: 'â–¶', color: '#22c55e' }),
  { id: 'a→b', type: 'link', source: { id: 'a', port: 'out' }, target: { id: 'b', port: 'in' }, style: { color: '#94a3b8', width: 2 } },
  { id: 'b→c', type: 'link', source: { id: 'b', port: 'out' }, target: { id: 'c', port: 'in' }, style: { color: '#94a3b8', width: 2 } },
];

// The element reads its own selected state — no context, no props threaded down.
function NodeCard({ label, description, icon, color }: NodeData) {
  const isSelected = useIsCellSelected();
  const className = isSelected ? 'node node--selected' : 'node';
  return (
    <HTMLHost useModelGeometry className={className}>
      <span className="node__icon" style={{ color }}>
        {icon}
      </span>
      <div>
        <div className="node__label">{label}</div>
        <div className="node__desc">{description}</div>
      </div>
    </HTMLHost>
  );
}

// The footer reads the selection collection reactively.
function Footer() {
  const { collection } = useSelection();
  const selectedIds = useCells(collection, (cells) => (cells ?? []).map((c) => String(c.id)));
  return (
    <footer className="footer">
      {selectedIds.length ? (
        <>Selected <code>{selectedIds.join(', ')}</code> · drag to move · click blank to clear</>
      ) : (
        'Click a node to select it · drag any node to move it'
      )}
    </footer>
  );
}

export default function App() {
  // <Diagram>'s built-in interactions wire selection for you: click selects,
  // Cmd/Ctrl+click toggles, blank-click clears. The <Selection> feature turns
  // it on for this Paper — no custom context or pointer-event handlers needed.
  return (
    <Diagram initialCells={initialCells}>
      <div className="stage">
        <PaperScroller mode="infinite" className="canvas">
          <Paper gridSize={10} renderElement={NodeCard} linkRouting={linkRoutingOrthogonal()}>
            <Selection wrapper={{ handles: [] }} />
          </Paper>
        </PaperScroller>
        <Footer />
      </div>
    </Diagram>
  );
}


Click an element to select it (the ring appears and the footer updates). Cmd/Ctrl+click to toggle, Shift+drag to rubber-band a region, click empty canvas to clear. Drag any element to move it.

How it works​

<Diagram> ships built-in interactions (on by default), so dropping the <Selection> feature into the Paper makes click select, Cmd/Ctrl+click toggle, region select, and blank-click clear work automatically:

function NodeCard() {
const isSelected = useIsCellSelected(); // this element's selected state
const className = isSelected ? 'node node--selected' : 'node';
return <HTMLHost useModelGeometry className={className}>…</HTMLHost>;
}

// <Selection> turns selection on for this Paper — no events to wire.
// wrapper={{ handles: [] }} keeps just the outline (no resize/rotate handles).
<Paper renderElement={NodeCard}>
<Selection wrapper={{ handles: [] }} />
</Paper>

// Read the live selection from anywhere inside <Diagram>
const { collection } = useSelection();
const selectedIds = useCells(collection, (cells) => (cells ?? []).map((c) => String(c.id)));

Two hooks connect selection to your React UI:

  • useIsCellSelected(): inside renderElement, subscribes an element to just its own selected state, so only the elements whose selection changed re-render.
  • useSelection(): the <Selection> feature's companion — from anywhere inside <Diagram>, exposes the shared selection collection, a selectCells setter, and the selection view itself (region gestures, select-all). Read the collection reactively with useCells(collection, …).

The <Selection> feature also brings a selection box, resize and rotate handles, and rubber-band region selection for free. This demo passes wrapper={{ handles: [] }} to keep just the selection outline; drop that to get the full handle set. Full options in Selection.

On @joint/react

The community edition has no built-in selection. You wire it yourself: hold the selected id in React state (or a small Context), set it from the Paper pointer events shown in Interactivity & Events, and read it back in each element. It's a few lines, but JointJS+ gives you the whole interaction, plus the box and handles, out of the box.

Next​

You have a selectable diagram. Next, add the JointJS+ editor building blocks around it, starting with an element palette.