Skip to main content
Version: 4.3

Element Selectors

useMarkup() lets you register named sub-elements inside a React-rendered node. Those names become JointJS selectors, so links, highlighters, and other features can target specific parts of the element instead of the whole node.

This works with both SVG elements and HTML elements inside HTMLHost or HTMLBox. It is most useful when a custom element has multiple meaningful targets, such as rows, handles, badges, or headers.

Basic usage​

This example builds two stacked nodes with row-level selectors. A link connects the Footer row to the Output row. Drag from any row to create a new link, or drag an existing endpoint to retarget it.

Only rows are valid connection targets in this example. Each row is registered through magnetRef, and the Paper is configured with validateConnection={{ allowRootConnection: false }} so drops on the empty area of an element are rejected.

Each row registers a selector such as row-0, row-1, or row-2. The link endpoints then target those selector names through source.magnet and target.magnet. The example also uses straightWhenDisconnected: false in the smooth routing preset so links stay curved while they are being dragged or retargeted.

How useMarkup() works​

useMarkup() returns two ref helpers:

const { selectorRef, magnetRef } = useMarkup();

Both register the node under a selector name: they set its joint-selector attribute and add it to the element view's selectors so JointJS features can target it. The difference is whether the node should also accept link connections:

  • selectorRef(name) registers the name only. Use it for nodes that are visual landmarks (highlighter targets, sub-regions you want to address by name).
  • magnetRef(name) registers the name and marks the node as an active magnet. Use it for nodes that should be valid link endpoints.

For target-only magnets (accept connections but don't initiate them), pass { passive: true }:

<rect ref={magnetRef('row-2', { passive: true })} ... />
Works with HTML too

Both selectorRef and magnetRef accept any DOM ref and behave the same way inside HTMLHost or HTMLBox content as they do on raw SVG. Useful when nodes are HTML cards with sub-regions (rows, handles, action buttons) that need to be addressable by name:

<HTMLHost>
<div ref={magnetRef('action')}>Connect from here</div>
</HTMLHost>

The most common pairing is:

  • ref={magnetRef('row-2')} on a sub-element, which registers the selector and marks the node as an active magnet in one call
  • source.magnet / target.magnet set to that selector name in the link record
  • validateConnection={{ allowRootConnection: false }} on Paper to keep drops from falling back to the element root
<rect ref={magnetRef('row-2')} ... />

const initialCells = [
{
id: 'a-b',
type: 'link',
source: { id: 'a', magnet: 'row-2' },
target: { id: 'b', magnet: 'row-2' },
},
];

This gives you selector-based connection targets without switching to ports or relying on fragile DOM structure like nth-child selectors. magnetRef does the registration and the magnet wiring at once, and source.magnet / target.magnet pick the registered selector for existing links.

allowRootConnection: 'auto' (the default) already blocks drops on an element's body once it has ports; set it to false to require a magnet even on elements without ports, which replaces the older pattern of setting attrs.root.magnet: false on every element. To match named targets, the callback form receives ConnectionEnd objects whose selector is the name registered by useMarkup(): validate: ({ source, target }) => source.selector === target.selector. The full rule set lives in Validating connections.

Connection highlights​

Two paper-level highlight events fire during a link drag. Both apply to the named selectors magnetRef registers, and @joint/react styles both via CSS by default:

  • magnetAvailability fires on every valid drop target as soon as the drag starts, so the user can see all the places the link could land. It is enabled by markAvailable on Paper. The default config adds the class jj-is-magnet-available to each available magnet.
  • connecting fires on the magnet under the cursor, the one the link will connect to if released right now. The default config strokes the magnet using the --jj-highlighter-connecting-color variable.

Customizing both is one CSS file:

app.css
:root {
--jj-highlighter-connecting-color: #3b82f6;
}

.jj-is-magnet-available > rect {
fill: rgb(59 130 246 / 0.08);
}

This selector assumes a <rect> child on the registered magnet. Adjust the descendant selector to match your structure.

The result is two layers of feedback: a soft blue fill on every valid target, and a solid blue stroke on the one under the cursor. You do not need a highlighting override on <Paper>; the variable cascades through the default config.

For HTML magnets inside HTMLHost or HTMLBox, drop the > rect and use background instead of fill.

To swap the highlighter type itself (for instance, to use a mask outline for connecting), configure it through the highlighting prop on <Paper>. See the highlighters catalog for the full set (mask, stroke, opacity, addClass, list).

Selectors are also useful when native JointJS features need to target a specific part by name, for example when highlighters.mask outlines one row instead of the whole element. Continue to Outlining Elements for a full mask-highlighter example.

highlighters.mask.add(elementView, 'row-1', 'hover', {
padding: 4,
});

That is the same selector mechanism used for row-level highlights, targeted tools, or other element-view operations.

Constraints and notes​

  • Use useMarkup() inside renderElement or a component rendered from it
  • Attach selectorRef() or magnetRef() to the DOM element you want JointJS to target
  • Keep selector names stable, especially when links or highlighters refer to them
  • The selector names __portal__, root, and portRoot are reserved and throw if used