Skip to main content
Version: 4.3

Links

In @joint/react, links live as cells in the same array as elements, distinguished by type: 'link'.

A link record answers four questions:

  • what does this link connect (source and target)
  • how should it travel (router and connector)
  • how should it look (style, labelStyle, and labelMap)
  • what extra application data should travel with it (data)

Most links only need declarative data. Reach for renderLink only when you need custom React-rendered SVG content on top of the link view.

Here is a complete example, broken down section by section below.

The nodes use the default renderer so the focus stays on the link: its route, arrowhead, and status label.

A link record is the shape of one link in graph state. These are the fields you will use most often:

{
id: 'design-auth',
type: 'link',
source: { id: 'design' }, // where the link starts
target: { id: 'auth' }, // where the link ends
router: { name: 'manhattan' }, // pathfinding strategy
connector: { name: 'rounded' }, // final path shape
data: { kind: 'hard' }, // your custom props
style: { targetMarker: 'arrow' }, // visual overrides for the built-in line
labelMap: {
status: { text: 'required' }, // declarative labels keyed by ID
},
}

source and target use standard JointJS link-end values. The most common forms are { id } and { id, port }. See Ports for declarative port definitions and how port IDs are attached to elements.

Link records are not limited to these fields. They extend JointJS link attributes, so advanced native properties like vertices, labels, and custom attrs can still pass through when needed.

source, target, router, and connector

Every link needs a source and target.

source: { id: 'start' },
target: { id: 'end' },

To connect to a specific port, include the port ID:

source: { id: 'start', port: 'out' },
target: { id: 'end', port: 'in' },

router and connector are forwarded to JointJS as-is:

  • router decides how the link finds a route between the ends
  • connector decides how that route is turned into a final SVG path

For many diagrams, you can omit both and accept the default route. Add them when you need orthogonal, rounded, stepped, or otherwise constrained links.

For paper-wide routing, set <Paper linkRouting> with one of the three presets from @joint/react. Each preset returns a routing bundle that wires up defaultRouter, defaultConnector, defaultAnchor, and defaultConnectionPoint together, coordinated as one instead of composing native router/connector by hand:

import { linkRoutingOrthogonal } from '@joint/react';

const ORTHOGONAL = linkRoutingOrthogonal({ cornerType: 'cubic', cornerRadius: 8 });

<Paper linkRouting={ORTHOGONAL} />

The three presets:

  • linkRoutingStraight(): direct line from source to target
  • linkRoutingOrthogonal(): orthogonal path with anchored midpoints
  • linkRoutingSmooth(): outward curve

For full control without a preset, pass a custom routing object directly to linkRouting (an object with the four keys defaultRouter, defaultConnector, defaultAnchor, defaultConnectionPoint), or drop to the options escape hatch on <Paper> for the full native dia.Paper.Options surface.

All three accept these shared options (mode and straightWhenDisconnected have no effect on linkRoutingStraight, which is already straight):

OptionMeaning
modeAnchor strategy. See LinkMode for the supported values.
sourceOffset / targetOffsetPushes the anchor away from the element edge in pixels.
markerSelectorWhich markup node the markers attach to.
straightWhenDisconnectedDefault true. Keeps disconnected ends as straight lines, then switches to the full routing once both ends connect.

Plus per-preset extras:

PresetSpecific options
linkRoutingStraightcornerType, cornerRadius, perpendicular
linkRoutingOrthogonalcornerType, cornerRadius, margin, minPathMargin
linkRoutingSmooth(shared only)

Per-link router and connector on the link record still take precedence over the bundle, so you can keep one paper-wide preset and override individual links when needed.

style with LinkStyle

Use style for per-link visual overrides on the built-in line and wrapper.

style: {
color: '#2563eb',
width: 2,
dasharray: '6,4',
targetMarker: 'arrow',
className: 'dependency-link',
wrapperWidth: 10,
wrapperColor: 'transparent',
}

LinkStyle groups properties for the line, markers, and wrapper. Markers can be passed as named strings or as LinkMarkerRecord values built from factory functions. See Link Marker Presets for the full set including ER/IE notation.

Use style for one-off exceptions. For a shared visual language across the whole diagram, prefer CSS variables or shared style objects and helpers in your app code.

If a style property is omitted, the theme can take over. See Theming for the full --jj-* variable surface.

The snippet above shows the API shape. The preview below contrasts a few common line-style variations using the built-in link renderer:

@joint/react exposes 16 marker factories. The five most common shapes plus 'none' also have plain-string aliases, so you can drop them straight into style.sourceMarker or style.targetMarker without imports.

Named markers

The named registry covers the everyday set:

  • 'arrow': filled triangle
  • 'arrow-open': open chevron
  • 'arrow-sunken': triangle with concave back
  • 'circle': filled circle
  • 'diamond': filled diamond
  • 'none': suppress the marker on that end
style: { targetMarker: 'arrow' }

The demo below shows each named marker on its own link. The element ports use portMap to anchor each link at a stable y position:

Marker factories

For markers outside the named registry, and for scale, fill, or stroke overrides on any marker, call the factory directly and pass the resulting LinkMarkerRecord to targetMarker or sourceMarker:

import { linkMarkerCross } from '@joint/react';

style: {
targetMarker: linkMarkerCross({ scale: 1.5 }),
}

All factories accept the same LinkMarkerOptions:

OptionDefaultMeaning
scale1Uniform scale of the marker shape.
fillinherits link colorSet to 'none' for outline-only.
strokeinherits link colorStroke color for outline shapes.
strokeWidth2Stroke width in pixels.

The full preset list:

FactoryShape
linkMarkerArrowFilled triangle.
linkMarkerArrowOpenOpen chevron / V.
linkMarkerArrowSunkenTriangle with concave back edge.
linkMarkerArrowQuillTriangle with split, quill-like back.
linkMarkerArrowDoubleTwo nested filled triangles.
linkMarkerCircleCircle.
linkMarkerDiamondDiamond / lozenge.
linkMarkerLineSingle perpendicular bar.
linkMarkerCrossX (two crossing lines).
linkMarkerForkInverted (backward-pointing) triangle.
linkMarkerForkCloseFork + bar at the tip.
linkMarkerOneSingle perpendicular bar, ER notation "exactly one".
linkMarkerOneOptionalBar + circle, ER "zero or one".
linkMarkerManyCrow's foot, ER "many".
linkMarkerManyOptionalCrow's foot + circle, ER "zero or many".
linkMarkerOneOrManyCrow's foot + bar, ER "one or many".

Six factories implement IE / Chen entity-relationship notation: the cardinality markers linkMarkerOne, linkMarkerOneOptional, linkMarkerMany, linkMarkerManyOptional, and linkMarkerOneOrMany, plus linkMarkerLine for "one". Useful when modelling database schemas or ER diagrams.

The demo below renders every factory between the two element columns. Drag the slider to scale all markers in lockstep and inspect how each shape responds to the scale option:

labelMap and labelStyle

Sugar over native labels

labelMap is a convenience layer over the native JointJS labels array: @joint/react converts each entry into a native label internally. Reach for native link labels when you need tangent-aligned rotation, explicit position.angle, or custom multi-node label markup via jsx().

Use labelMap when a link needs one or more labels.

labelMap: {
status: {
text: 'approved',
position: 0.5,
color: '#1e293b',
backgroundColor: 'white',
backgroundOutline: '#cbd5e1',
backgroundPadding: { horizontal: 8, vertical: 4 },
backgroundShape: 'rect',
},
}

Each entry in labelMap is keyed by a stable label ID like status, yes, or error. That makes labels easier to update than working with a positional array.

LinkLabel covers placement, text styling, and a background shape with its own styling. If several labels on the same link should share the same look, put the shared styles in labelStyle and let each labelMap entry override only what is different.

warning

Setting both labelMap and native labels on the same link throws. Pick one representation and stick to it.

The snippet above shows the declarative shape. The preview below uses labelStyle for shared defaults and lets individual labels override only what is different:

Experimental

renderLink is experimental and its API may change in future releases.

Paper.renderLink exists, but treat it as an advanced and experimental escape hatch.

Use it when you need custom React-rendered SVG content on a link, not for everyday styling. The built-in line, markers, and labels already cover most cases.

function DependencyOverlay(data: DependencyData) {
const layout = useLinkLayout();
if (!layout) return null;
return <path d={layout.d} />;
}

<Paper renderLink={(data) => <DependencyOverlay {...data} />} />

renderLink receives the link's data, not its geometry. If the renderer needs path coordinates, use useLinkLayout() inside the rendered component.

If the custom React content should be the main visual, keep the regular link style minimal so the built-in line does not compete with it.

The snippet above is the minimal escape-hatch shape. The preview below adds a custom SVG overlay on top of a subdued built-in link:

When a user drags from a magnet to create a connection, JointJS creates a default link. The defaultLink prop on <Paper> decides what that link looks like.

For a single static default, pass a link record value: the same shape this chapter describes (without id and type, which JointJS supplies for the new link):

<Paper defaultLink={{
style: { color: '#2563eb', width: 2, targetMarker: 'arrow' },
}} />

For per-source customization, pass a function instead. It receives { source, paper, graph } and returns the link config:

<Paper
defaultLink={({ source }) => ({
style: {
color: source.selector === 'output' ? '#10b981' : '#94a3b8',
width: 2,
targetMarker: 'arrow',
},
})}
/>

source is a ConnectionEnd: { id, model, port, magnet, selector }. Branch on whichever field you need.

For controlling which drag attempts succeed at all, see Validating connections below.

Validating connections

validateConnection decides whether an in-progress connection is allowed while the user drags a link. Even with no configuration, Paper applies sensible defaults: no self-loops, no link-to-link connections, no duplicate same-direction links, and root drops only when the element has no ports.

To adjust the built-in rules, pass an options object:

<Paper validateConnection={{
allowSelfLoops: false, // default — an element may not link to itself
allowLinkToLink: false, // default — links may not start or end on another link
linkLimit: 'one-per-direction', // default — see below
allowRootConnection: 'auto', // default — block root drops only when the element has ports
}} />

linkLimit caps how many links may connect the same source+port and target+port. Matching is port/magnet aware, so links on different ports never collide:

  • 'none' — no limit; any number of links, in either direction.
  • 'one-per-direction' (default) — one link each way: a second A → B is blocked, but the reverse B → A is allowed.
  • 'one-per-pair' — one link per element pair: A → B blocks both another A → B and the reverse B → A.

allowRootConnection is tristate: 'auto' (default) blocks root drops only when the element has ports; false always rejects them (require a port or magnet); true always allows them.

For rules beyond the built-ins, pass a function. It receives { source, target, endType, paper, graph } and returns a boolean. source and target are ConnectionEnd objects ({ id, model, port, magnet, selector }); magnet is the SVG element under the cursor, so to compare names registered by useMarkup() use selector. endType ('source' or 'target') tells you which end is being dragged, which helps with asymmetric rules:

<Paper validateConnection={({ target }) =>
target.port === 'in'
} />

Both forms combine: the options object accepts a validate function that runs after the built-in rules pass:

<Paper validateConnection={{
allowRootConnection: false,
validate: ({ source, target }) => source.selector === target.selector,
}} />

Connection strategy

Paper.connectionStrategy decides where a link endpoint lands when the user drops it. By default JointJS positions the anchor and connection point against the target element's defaults; a strategy lets you override that: snap to a port, pin to a specific paper coordinate, or rewrite the endpoint entirely. JointJS ships built-in strategies under connectionStrategies.

Pass the strategy directly to <Paper connectionStrategy>: either an options object with a pin mode and an optional customize callback, or just a customize callback on its own:

<Paper
connectionStrategy={{ pin: 'absolute' }}
/>

pin accepts:

  • 'none' (default): no pinning; the anchor follows its element
  • 'absolute': pin the anchor at the absolute paper coordinate where the user dropped the end
  • 'relative': pin the anchor as a percentage of the target element

For more control, pass a customize callback. Common pattern: route the dropped end to whichever side of the element the user actually aimed at, so the link enters from the natural direction:

<Paper
connectionStrategy={({ end, model, dropPoint }) => {
if (!model.isElement()) return end;
const side = model.getBBox().sideNearestToPoint(dropPoint);
return { ...end, anchor: { name: side } };
}}
/>

Pin modes can pin coordinates, but only customize can read the model and dropPoint together to make a structured decision: picking a side anchor, routing through a port, or rewriting EndJSON based on the link's own data.

The customize callback receives a context object with the dropped end (already pinned by the pin mode), the target model and magnet element, the dropPoint in paper coordinates, the endType, and the active link, paper, and graph instances.

Return the new EndJSON to commit, or return end unchanged to fall through to the default behaviour.

connectionStrategy runs after validateConnection accepts the drop, so the connection is already legal; the strategy only reshapes where it lands. Pair it with defaultLink for full control over the link-creation UX.

Quick reference

Cells are an array of records. Each link record carries id, type: 'link', and the fields below.

Connections

  • source: the link start. Common values are { id } or { id, port }.
  • target: the link end. Common values are { id } or { id, port }.

Path

  • router: optional route-finding strategy. It defaults to the underlying JointJS paper behavior.
  • connector: optional path-shaping strategy. It defaults to the underlying JointJS paper behavior.

Data and styling

  • data: custom user data passed to renderLink.
  • style: per-link overrides for the built-in line and wrapper.

Labels

  • labelMap: declarative labels keyed by label ID.
  • labelStyle: shared defaults for every label in labelMap.
  • linkRouting: paper-wide routing bundle (linkRoutingStraight / linkRoutingOrthogonal / linkRoutingSmooth).
  • defaultLink: shape of the link created when the user drags from a magnet.
  • connectionStrategy: where the dropped end of a link lands.