Skip to main content
Version: 4.3

Halo

<Halo /> mounts a ring of contextual handles around the selected cell. On an element those are rotate, remove, clone, fork, draw a new link, and unlink. Reach for it when you want the user to manipulate the selection without dragging a separate floating toolbar around the canvas.

The example below mounts <Halo /> on whichever cell is selected. Selection state lives in the <Diagram> root (see Selection); the demo mounts a <Selection> so <Diagram>'s built-in click-to-select keeps that state, and the halo appears whenever exactly one cell is selected.

Contextual handles on the selected element

Click a card (or a link between cards) to select it; the halo's ring of handles mounts on the selected cell and follows your selection around the canvas. Review starts selected so the ring is visible straight away; clicking blank space dismisses it.

A mounted <Selection> switches on <Diagram>'s built-in click-to-select and blank-to-clear, so the demo binds no <Paper> events of its own. While exactly one cell is selected, <Halo cell={cellId} /> mounts on it; cell accepts an id or a dia.Cell. <Halo> is selection-agnostic: it attaches to whatever cell you hand it, and the demo reads the current selection to pass the single selected id. Because the demo turns on <Selection frames />, a dashed frame also wraps that cell; the solid selection-wrapper box appears only when more than one cell is selected.

Wiring selection yourself

The demo leans on the Built-in interactions for click-to-select, region drag, and keyboard shortcuts. To drive selection by hand instead, turn them off and wire <Paper> event props to selectCells (see Programmatic control).

Built-in handles

<Halo /> ships with the following handles on an element:

  • rotate: drag to rotate; rotation snaps to rotateAngleGrid (degrees, default 15).
  • remove: delete the element from the graph.
  • clone: drag to duplicate the element.
  • fork: drag to spawn a new element of the same type, linked to this one.
  • link: drag to draw a new link from this element to another element on the paper.
  • unlink: disconnect every link attached to the element.

A link carries a different ring: remove plus a direction handle that flips its source and target. <Halo> picks the handle set from the selected cell's type, so the same <Halo cell={cellId} /> serves both; click one of the connectors in the demo above, flip it, and the arrowhead jumps to the other end.

Handles sit in eight compass groups (nw, n, ne, e, se, s, sw, w) around the element. See the ui.Halo API page for the full handle and group reference.

Adding a resize handle

The resize handle is not part of the default element halo; add it with getHaloDefaultHandle('resize') (see Filtering the handle set). It changes an element's size on the model, so it needs the element to render at its model geometry: an element drawn with a plain <HTMLHost> (or <HTMLBox>) measures its own content and writes that size back, immediately undoing the resize. Give the element an explicit size and render it with useModelGeometry; then dragging the handle resizes the element.

Filtering the handle set

To restrict the halo to a subset of the built-in handles, pass the handles prop. The prop replaces the default set wholesale; there is no additive mode. Use getHaloDefaultHandle('<name>') to keep the default behaviour of the handles you do want:

import { getHaloDefaultHandle, Halo } from '@joint/react-plus';

<Halo
cell={targetId}
handles={[
getHaloDefaultHandle('remove'),
getHaloDefaultHandle('rotate'),
]}
/>;

getHaloDefaultHandle is exported from @joint/react-plus directly, so you don't need to reach into the @joint/plus ui namespace for it.

handles also accepts a function, (defaultHandles) => HaloHandle[]. The function receives the per-cell-type defaults Halo would have applied for the current cell (different defaults for elements vs links). Use this form when you want to filter or tweak individual handles without re-listing the rest:

<Halo
cell={targetId}
handles={(defaults) => defaults.filter((handle) => handle.name !== 'fork')}
/>

Custom handles

Reach for custom Halo handles when the interaction is a gesture, a sequence that spans pointerdown to drag to pointerup. Every built-in Halo handle is one of these (forking off a new element, drawing a link, dragging out a clone, rotating, resizing). A custom handle slots into the same drag machinery: spread a default handle, swap its content, and give it its own makeElement / makeLink closure that returns the model to create.

For single-click actions that mutate the element's data (recolour, rename, toggle a flag, open a side panel), reach for <Overlay> / <ElementOverlay> + setCell instead. That path stays in plain JSX and React state, with nothing imperative to wire up.

The example below replaces Halo's default fork handle with three drag variants, one per workflow status, stacked down the element's east side (a groups spacing keeps them apart). Each is a white + badge in its status colour; drag one to spawn a new step with that status, linked to the source. Click a different card to move the halo (and its custom handles) to that element.

  • getHaloDefaultHandle('fork') returns the canonical fork-handle config; the pointerdown / pointermove / pointerup events that drive Halo's startForking / doFork / stopForking are already wired. Spread it, then override group, title, content, and makeElement to re-skin the handle without touching the drag.
  • content accepts a React node (or an HTML string). Halo renders it to static markup, since handle content is presentational; the click is handled by ui.Halo.
  • makeElement lives on the handle that triggers creation, as a closure. It captures whatever the new element needs, so its data stays typed with no data round-trip. Return a record (Partial<ElementRecord>), which Halo coerces to an ElementModel, or return a model instance. Give a handle a makeLink for a custom link; otherwise Halo uses the paper's default link.
  • name is optional. Halo auto-assigns a unique id when it is missing or collides, so spreading the same default handle several times (as the three fork buttons do) just works. Pass your own name for a stable id.

You can mix as many fork variants as the layout allows, one handle per compass group (n, ne, e, se, and so on), or place them in a custom layout slot; see Custom groups below.

Custom groups

groups let you re-position handles or define new layout slots. Each entry positions a group relative to the element's bounding box, with trackDirection controlling row vs column flow:

import { getHaloDefaultHandle, Halo } from '@joint/react-plus';

<Halo
cell={targetId}
groups={{
sidebar: {
top: '0',
left: 'calc(100% + 8px)',
verticalAlign: 'top',
horizontalAlign: 'left',
trackDirection: 'column',
trackCount: 2,
gap: '4px',
},
}}
handles={[
{ ...getHaloDefaultHandle('remove'), group: 'sidebar' },
{ ...getHaloDefaultHandle('clone'), group: 'sidebar' },
]}
/>;

Handles whose group is 'sidebar' flow into the column. See the groups option on ui.Halo for the full set of group properties (trackDirection, gap, className, etc.).

Inside renderElement

<Halo> can render inline inside a renderElement callback. Drop the cell prop; the cell is taken from the surrounding context:

function TaskWithHalo({ label, status }: ElementData) {
return (
<>
<TaskCard label={label} status={status} />
<Halo rotateAngleGrid={15} />
</>
);
}

Use this when every element of a given type should always carry a halo, with no separate selection wiring.

Styling with CSS variables

The halo exposes a small set of CSS variables for tweaking handle size and spacing without re-implementing the markup. Defaults shown; override any in your own stylesheet:

.jj-halo {
--jj-halo-group-offset: var(--jj-space-lg);
--jj-halo-handle-width: 14px;
--jj-halo-handle-height: 14px;
--jj-halo-handle-font-size: 14px;
--jj-halo-handle-color: var(--jj-color-primary);
}