Resize & Rotate
<FreeTransform /> mounts interactive resize and rotation handles around a single graph element. Reach for it when you want users to resize or rotate the selected element directly on the canvas, without a separate sizing dialog.
The example below mounts <FreeTransform /> on whichever element is selected. Selection state lives in the <Diagram> root (see Selection), so a click handler updates it directly, with no <Selection> component required; the handles appear whenever exactly one element is selected.
Resize the selected element
Click a card to select it; the resize and rotation handles mount on the selected element and follow your selection from card to card. Review starts selected so the handles are visible straight away; clicking blank space dismisses them. Drag a corner to resize, or the handle above the top-left corner to rotate.
Selection is wired by hand: onElementPointerClick adds the clicked element with selectCells, and onBlankPointerClick clears it, both from useSelection(). A small component reads the live selection collection through useCells and mounts <FreeTransform cell={…} /> while exactly one element is selected. Because the selection state lives in the <Diagram> root, this needs no <Selection> component. Reach for <Selection> only when you also want its frame and region-drag.
<FreeTransform> changes an element's size on the model. 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, as the cards here do; then dragging a handle resizes the element.
The demo wires selection manually because we are building custom UI around the paper. If you only need standard gestures, drop in Built-in interactions, which cover click-to-select, region drag, keyboard shortcuts, and more.
Aspect ratio and rotation
preserveAspectRatio locks the width-to-height ratio while resizing. The four edge handles are hidden while it's on. Only the corner drags remain available, and they scale the element uniformly.
Rotation is off by default for <FreeTransform>. Pass allowRotation to add a rotate handle above the top-left corner. rotateAngleGrid (degrees) snaps the angle to multiples of the value and defaults to 15; set it to 1 for effectively continuous rotation:
<FreeTransform
cell={targetId}
allowRotation
preserveAspectRatio
rotateAngleGrid={45}
/>
Size constraints
minWidth, minHeight, maxWidth, and maxHeight clamp the element's size during resize. Pass a literal pixel value for a global limit:
<FreeTransform
cell={targetId}
minWidth={120}
minHeight={80}
maxWidth={400}
/>
Pass a function for a per-cell or direction-aware limit. The callback receives the cell, the underlying ui.FreeTransform instance, and the direction being dragged ('top-left', 'top', 'top-right', …):
<FreeTransform
cell={targetId}
minWidth={(cell) => (cell.get('locked') ? cell.size().width : 80)}
maxWidth={(_cell, _ft, direction) => (direction === 'right' ? 600 : Infinity)}
/>
The function form is the practical reason to reach for these props: most apps have rules like "locked cells can't shrink" or "footers can't grow taller than 120px" that don't fit into a single numeric limit.
Snap, directions, and borders
resizeGrid: { width, height } snaps the resize step to a grid. Match it to the paper's gridSize to keep elements aligned with the background grid:
<FreeTransform cell={targetId} resizeGrid={{ width: 10, height: 10 }} />
resizeDirections restricts the visible handles to a subset of 'top-left' | 'top' | 'top-right' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left'. Omitting a direction hides its handle and disables resize along that edge:
<FreeTransform
cell={targetId}
resizeDirections={['top-left', 'top-right', 'bottom-right', 'bottom-left']}
/>
useBordersToResize hides the four edge handles and stretches them across the element's borders as invisible drag zones, so users grab the borders directly. The corner pucks stay visible. Pair it with a CSS rule that highlights the border on hover so the affordance stays discoverable. padding controls the visual gap between the element's bounding box and the handles (defaults to 4):
<FreeTransform cell={targetId} useBordersToResize padding={6} />
allowOrthogonalResize is true by default and exposes the four edge handles in addition to the corners. Set it to false when corner-only resizing matches your interaction model.
Styling with CSS variables
The handle size is exposed as a CSS variable. The default is 8px for both the resize and rotate pucks; override it in your own stylesheet to enlarge or shrink them:
.jj-free-transform .resize,
.jj-free-transform .rotate {
--jj-ft-handle-size: 8px;
}
--jj-ft-handle-size sets each puck's width and height. The handle's offset from the bounding box is derived from the handle size (and the border width), so it re-centers automatically as the size changes. The preset exposes more variables on :root for deeper theming, including --jj-ft-border-color, --jj-ft-handle-background, --jj-ft-handle-border-color, and --jj-ft-handle-radius.
Auto-scroll
Inside a <PaperScroller>, dragging a resize or rotate handle past the visible edge scrolls the viewport automatically, with no extra wiring. It follows the scroller's scrollWhileDragging setting; disable it there to clamp at the edge instead.
Inside renderElement
<FreeTransform> can render inline inside a renderElement callback. Drop the cell prop, and the cell is taken from the surrounding context:
function FrameRenderer({ label, color }) {
return (
<>
<FreeTransform minWidth={120} minHeight={80} />
<HTMLHost style={{ backgroundColor: color }}>
<span>{label}</span>
</HTMLHost>
</>
);
}
Use this when every element of a given type should always carry its own transform handles.