Escape hatches
@joint/react and @joint/react-plus wrap several JointJS configuration shapes in React-friendly sugar APIs. The raw JointJS shape is always still reachable on the same record or component, and that raw shape is the escape hatch.
Most diagrams never need to leave the sugar. Reach for an escape hatch when:
- you need a JointJS option the React API doesn't expose as a dedicated prop or field
- you want to mirror an existing native JointJS configuration you already have
- you need to work with JointJS's view layer directly
Component options
options is one concept across both packages: on any component, @joint/react or @joint/react-plus, the options prop is a raw passthrough to the underlying JointJS view's constructor, for anything that doesn't have a dedicated React prop. Most configuration is exposed as typed props (<Paper> has gridSize, drawGridSize, linkRouting, and so on; the JointJS+ components have their own); options carries the rest.
<Paper>forwards todia.Paper.Options.<PaperScroller>,<Selection>,<Halo>,<Navigator>,<Stencil>,<FreeTransform>,<Snaplines>forward to the matchingui.*.Options.
<Paper
gridSize={10}
options={{
allowLink: (linkView) => linkView.model.get('source').id !== linkView.model.get('target').id,
restrictTranslate: true,
}}
/>
<Halo options={{ useModelGeometry: true }} />
<PaperScroller options={{ padding: 80, minVisiblePaperSize: 200 }} />
Values inside options override matching keys set as top-level props. Treat options as the authoritative form when you need raw JointJS access.
@joint/react-controlled optionsasync, sorting, viewManagement, frozen, and autoFreeze are set by @joint/react internally; the portal rendering depends on the values it picks. Overriding any of them via options might result in unexpected rendering behavior.
Reaching the underlying view
React renders each shape's content, but the foundations underneath it (the paper, link views, ports, tools, and highlighters) are drawn by JointJS's own mvc.View layer. Every React component sits on a raw JointJS instance from that layer: a dia.Paper for <Paper>, a ui.* view from @joint/plus for the JointJS+ components. Reach it through a ref, a hook, or both when the declarative props and hooks don't cover what you need. Its type comes from @joint/plus (dia.Paper or the matching ui.*), not from the React packages, and it is null until the component mounts.
| Component | Instance | Ref | Hook |
|---|---|---|---|
<Paper> | dia.Paper | yes | usePaper().paper |
<PaperScroller> | ui.PaperScroller | yes | usePaperScroller().paperScroller |
<Selection> | ui.Selection | yes | useSelection().selection |
<Stencil> | ui.Stencil | yes | useStencil().stencil |
<Halo> | ui.Halo | yes | n/a |
<Navigator> | ui.Navigator | yes | n/a |
<FreeTransform> | ui.FreeTransform | yes | n/a |
<Snaplines> | ui.Snaplines | yes | n/a |
import type { ui } from '@joint/plus';
// Hook: the view is on the handle (null until it mounts).
const { paperScroller } = usePaperScroller();
// Ref: the view itself.
const haloRef = useRef<ui.Halo>(null);
<Halo ref={haloRef} />;
The hooks are the primary React API: useSelection() exposes selectCells / startSelectionRegion, usePaperScroller() exposes setZoom / zoomToFit, and so on. Reach the raw view only when that API is missing a method you need:
usePaper().paper?.transformToFitContent({ padding: 20 });
usePaperScroller().paperScroller?.transitionToRect(rect);
The views are not stable references, and they exist only after the component mounts, so guard each access with ?. and read the view fresh instead of caching it.
Native ports
portMap and portStyle cover single-group, absolutely-positioned ports with a single-shape body, which is enough for most diagrams. Drop to native ports when you need:
- parametric port layouts like
ellipseSpread,line, and edge-distribution layouts that auto-space ports along a curve or side - multiple port groups on one element, each with its own positioning, sizing, and styling
- custom port markup for multi-node port bodies, named selectors, and raw
attrsper port
The demo below uses the ellipseSpread layout to fan ports around the perimeter of each ellipse, with radialOriented labels following the angle. Neither layout is reachable from portMap.
A native ports object has two layers: groups defines shared positioning, markup, and attrs for a set of ports; items lists the individual port IDs and assigns each to a group. For the full set of options, see Ports. Pick a group position.name from the built-in port layouts (absolute, left, right, top, bottom, line, ellipseSpread) and a label position from the port label layouts.
Setting both portMap and native ports on the same element throws. Pick one representation per element.
Native link labels
labelMap and labelStyle cover the common label shape (text, position, offset, padded background), and @joint/react converts each entry into an item on the link's native labels array internally. Drop to the native labels array when you need:
- tangent-aligned labels that rotate to follow the link direction via
position.angleandposition.args.keepGradient - custom label markup for multi-node label bodies (icon + text + badge) with your own named selectors via
jsx() - raw SVG
attrsper selector for gradients, filters, transforms, and anything outside the curatedLinkLabelsurface
The demo below has three nodes, with two links starting from A. The A → B link carries two labels: one that auto-rotates to follow the link tangent (keepGradient) and one with an explicit angle rotation. The A → C link carries a label with three-node markup (background rect + icon glyph + text) built via jsx(), each node addressable by its own selector.
A native label is an entry in the link record's labels array. Each entry has its own markup (authored with jsx()), attrs keyed by selector, and position (distance along the link plus optional offset, angle, and args). For the full schema see link.label().
Setting both labelMap and labels on the same link throws. Pick one representation per link.