Skip to main content
Version: 4.3

Zoom & Scroll

<PaperScroller /> wraps a <Paper /> in a scrollable, zoomable viewport. Reach for it when your diagram is larger than the visible area, or when you need a controllable camera (pan, zoom, page-grid, programmatic moves) without writing the gesture plumbing yourself.

info

This chapter covers the JointJS+ PaperScroller component from @joint/react-plus. For an overview minimap that drives the same viewport, see Minimap.

Wrapping a Paper

The minimum shape is a <PaperScroller> around a <Paper>:

import { GraphProvider, Paper, PaperScroller } from '@joint/react-plus';

export default function App() {
return (
<GraphProvider initialCells={initialCells}>
<PaperScroller style={{ width: '100%', height: 420 }}>
<Paper renderElement={renderElement} />
</PaperScroller>
</GraphProvider>
);
}

The scroller fills its wrapper, so size it through className, style, or a flex/grid parent.

A complete example

The demo below assembles the full control surface: an infinite/sheets mode switch, zoom buttons with a live percentage badge, a fixed-sheet lock, a page-break grid you can toggle on in either mode, and a page-size picker (A4, Letter, A3) that drives both the sheet and the grid. Drag the blank canvas to pan, use the buttons to zoom, switch modes, toggle the page grid, and change the page size.

Blank-drag pan and wheel/pinch zoom come from the <Diagram> built-in interactions, so the demo wires no gestures of its own. The toolbar renders outside the <PaperScroller> subtree, yet its controls reach the scroller with the no-arg hooks: usePaperScroller() for the zoom buttons and usePaperScrollerViewport() for the live %, both of which resolve the single paper automatically.

Each piece has its own section below: Sheets mode for the page-sized canvas, Page breaks for the page-grid, Programmatic control for the usePaperScroller handle, and Reactive viewport status for the live readout.

Infinite mode

mode="infinite" is the default: a borderless canvas that grows in every direction as content moves outward. It suits open-ended diagrams where the user arranges nodes freely and the surface should never get in the way. There is nothing to configure; it is what you get from a plain <PaperScroller>.

Sheets mode

mode="sheets" gives a bordered canvas sized in sheetWidth × sheetHeight increments. New pages appear as content extends past the current bounds, which suits print-oriented or paginated layouts.

<PaperScroller mode="sheets">
<Paper />
</PaperScroller>

sheetWidth / sheetHeight set the page size; they default to 595 × 842 (A4 portrait at 72 DPI).

fixedSheet locks the canvas to a single sheet at (sheetX, sheetY) instead of letting it grow:

<PaperScroller
mode="sheets"
fixedSheet
sheetWidth={800}
sheetHeight={600}
sheetX={0}
sheetY={0}
>
<Paper />
</PaperScroller>

sheetX / sheetY shift the page-grid origin in growth mode, or position the locked sheet's top-left in fixedSheet mode. They are ignored in infinite mode.

Because a locked sheet no longer grows, an element dragged past its edge is clipped at the boundary by default. The demo above sets the overflow prop on <Paper> so elements stay visible beyond the sheet instead; drop it if you want the sheet's hard limit to clip them.

Page breaks

<PageBreaks> renders dashed page-grid lines wherever you want sheet boundaries visible. It works in either mode, infinite or sheets, so the demo lets you toggle it on in both; the only case where it makes no sense is a fixedSheet canvas, a single non-growing page with nothing to break up.

Mount it as a child of <Paper>. Its width and height set the spacing between break lines, independent of <PaperScroller sheetWidth/sheetHeight>. Pass the same values when you want the break grid to line up with the sheet boundaries. x and y offset the grid origin (both default 0); the lines fall at every (x + n*width, y + n*height), so match them to the scroller's sheetX / sheetY when a shifted sheet should still line up:

<Paper>
<PageBreaks width={800} height={600} />
</Paper>

Both props default to A4 (595 × 842), so omitting them matches the sheet-mode default.

Zoom bounds

minZoom and maxZoom clamp every zoom call: both the buttons in the demo above and the underlying ui.PaperScroller wheel/pinch handlers when Built-in interactions wire them up. Defaults are 0.2 and 3:

<PaperScroller minZoom={0.5} maxZoom={2}>
<Paper />
</PaperScroller>

setZoom accepts an optional anchor: pass { ox, oy } in paper coordinates to zoom around a specific point instead of the viewport center:

setZoom((previous) => previous + 0.2, { ox: 400, oy: 300 });

Programmatic control

usePaperScroller() is the React entry point to the scroller. Called with no argument it resolves the single default paper's scroller from anywhere within the <Diagram> component.

Targeting a specific paper

usePaperScroller() resolves the single default paper automatically. Pass a paper id (usePaperScroller(paperId)) to target a specific paper when your app renders more than one.

The handle is stable: it never throws and is never null, and its methods are safe to call immediately, since they no-op until the scroller mounts. It exposes:

  • paperScroller: the raw ui.PaperScroller instance, for anything without a dedicated prop (center(), centerContent(), and so on). Unlike the handle, this field is not stable: it is null until the paper and scroller have mounted, so guard it (not the handle) on every read instead of caching it.
  • setZoom(value, options?): value is a number or (previous) => next, clamped to minZoom / maxZoom; options.ox / options.oy set the zoom anchor.
  • zoomToFit(options?): scale the paper so its content fits the viewport. contentMargin sets the breathing room (default 40, 0 for edge-to-edge); other options forward to ui.PaperScroller.zoomToFit().
  • startPaperPan(event): start a pan from a pointer event; wire it to any gesture.
zoomToFit();                     // default 40-unit margin
zoomToFit({ contentMargin: 0 }); // edge-to-edge
Built-in interactions

The demo above gets its pan and zoom gestures from the <Diagram> Built-in interactions, which wire blank-drag pan, wheel pan (or wheel zoom via wheelAction: 'zoom'), Ctrl/Cmd + wheel and pinch zoom directly to PaperScroller in a single call. Reach for usePaperScroller and manual useOnPaperEvents wiring (startPaperPan bound to onBlankPointerDown) only when you want gestures the defaults don't cover, or custom UI like the toolbar buttons on top of that baseline.

Reactive viewport status

usePaperScroller() is imperative: its handle never re-renders when zoom or scroll changes. When you want UI that tracks the live viewport (a zoom indicator, a "zoom is at max" hint, a coordinate readout), reach for usePaperScrollerViewport(). Pass it a selector to subscribe to a single slice of the viewport, so the component re-renders only when that value changes, not on every pan:

import { usePaperScrollerViewport } from '@joint/react-plus';

function ZoomIndicator() {
const zoom = usePaperScrollerViewport((viewport) => viewport.zoom);
const zoomPercent = Math.round(zoom * 100);

return <span className="zoom-indicator">{zoomPercent}%</span>;
}

The viewport is always present, so the selector never has to null-check it: before a <PaperScroller> is registered the hook returns a neutral default (zoom: 1, zooming disabled, empty visibleArea). Because the selected value here is a single number, panning the canvas (which leaves the zoom untouched) triggers no re-render.

Call it without a selector to read the whole viewport object, which is likewise always defined:

const viewport = usePaperScrollerViewport();
// viewport.zoom, viewport.canZoomIn, viewport.canZoomOut, viewport.visibleArea

PaperScrollerViewport carries:

  • zoom: current scale (1 = 100%).
  • canZoomIn / canZoomOut: booleans against the configured minZoom / maxZoom.
  • visibleArea: the viewport's bounding box in graph coordinates, as a g.Rect.