Overview
The useSelectionBounds hook calculates and returns the bounding box (minimum enclosing rectangle) for all currently selected layers on the canvas. It’s essential for rendering selection UI, positioning tools, and handling multi-layer transformations.
Usage
import { useSelectionBounds } from '@/hooks/use-selection-bounds';
function SelectionBox() {
const bounds = useSelectionBounds();
if (!bounds) return null;
return (
<rect
x={bounds.x}
y={bounds.y}
width={bounds.width}
height={bounds.height}
className='selection-outline'
/>
);
}
Parameters
This hook does not accept any parameters.
Return Value
Returns the bounding box coordinates and dimensions, or null if no layers are selected.XYWH Type:type XYWH = {
x: number; // Left edge of the bounding box
y: number; // Top edge of the bounding box
width: number; // Total width of the bounding box
height: number; // Total height of the bounding box
};
How It Works
- Get Selection: Retrieves the current user’s selected layer IDs from presence state
- Fetch Layers: Maps selection IDs to actual layer objects from storage
- Calculate Bounds: Computes the minimum bounding box that encompasses all selected layers:
- Finds the leftmost x-coordinate
- Finds the topmost y-coordinate
- Finds the rightmost edge (x + width)
- Finds the bottommost edge (y + height)
- Calculates the final width and height
- Returns: The calculated bounding box or
null if no layers are selected
Real-World Examples
// source/app/board/[boardId]/_components/selection-tools.tsx:78
import { useSelectionBounds } from '@/hooks/use-selection-bounds';
export const SelectionTools = ({ camera, setLastUsedColor }) => {
const selectionBounds = useSelectionBounds();
if (!selectionBounds) return null;
// Position the toolbar above the selection
const x = selectionBounds.width / 2 + selectionBounds.x + camera.x;
const y = selectionBounds.y + camera.y;
return (
<div
className='absolute p-3 rounded-xl bg-white shadow-sm border'
style={{
transform: `translate(calc(${x}px - 50%), calc(${y - 16}px - 100%))`,
}}
>
<ColorPicker onChange={setFill} />
{/* Other tools */}
</div>
);
};
Rendering Selection Box
// source/app/board/[boardId]/_components/selection-box.tsx:24
import { useSelectionBounds } from '@/hooks/use-selection-bounds';
import { Side, XYWH } from '@/types/canvas';
const HANDLE_WIDTH = 8;
export const SelectionBox = ({ onResizePointerDown }) => {
const bounds = useSelectionBounds();
if (!bounds) return null;
return (
<>
{/* Main selection rectangle */}
<rect
className='fill-transparent stroke-blue-500 stroke-2'
style={{
transform: `translate(${bounds.x}px, ${bounds.y}px)`,
}}
x={0}
y={0}
width={bounds.width}
height={bounds.height}
/>
{/* Resize handles */}
<rect
className='fill-white stroke-1 stroke-blue-500'
style={{
cursor: 'nwse-resize',
width: `${HANDLE_WIDTH}px`,
height: `${HANDLE_WIDTH}px`,
transform: `translate(
${bounds.x - HANDLE_WIDTH / 2}px,
${bounds.y - HANDLE_WIDTH / 2}px
)`,
}}
onPointerDown={(e) => {
e.stopPropagation();
onResizePointerDown(Side.Top + Side.Left, bounds);
}}
/>
{/* More resize handles... */}
</>
);
};
Implementation Details
The hook uses a boundingBox helper function that:
- Returns
null for empty selections
- Initializes bounds from the first layer
- Expands bounds to include each subsequent layer
- Calculates the final width and height from the outer edges
const boundingBox = (layers: Layer[]): XYWH | null => {
const first = layers[0];
if (!first) return null;
let left = first.x;
let right = first.x + first.width;
let top = first.y;
let bottom = first.y + first.height;
for (let i = 1; i < layers.length; i++) {
const { x, y, width, height } = layers[i];
if (left > x) left = x;
if (right < x + width) right = x + width;
if (top > y) top = y;
if (bottom < y + height) bottom = y + height;
}
return {
x: left,
y: top,
width: right - left,
height: bottom - top,
};
};
The hook uses shallow comparison from @liveblocks/react to prevent unnecessary re-renders when the bounding box values haven’t actually changed.
Common Use Cases
- Selection Visualization: Draw a rectangle around selected layers
- Tool Positioning: Place editing tools relative to the selection
- Resize Operations: Provide resize handles at the bounds corners and edges
- Collision Detection: Check if selections intersect with other elements
- Batch Transformations: Apply transformations to multiple layers as a group
Type Definitions
type Layer = RectangleLayer | EllipseLayer | PathLayer | TextLayer | NoteLayer;
type XYWH = {
x: number;
y: number;
width: number;
height: number;
};
Always check if the returned value is null before using it. The hook returns null when no layers are selected or when selected layers no longer exist in storage.
Best Practices
- Always handle the
null case in your component rendering logic
- Use the bounds for positioning UI elements that should appear near the selection
- Combine with camera coordinates when rendering in screen space
- Cache expensive calculations that depend on bounds using
useMemo
- Consider debouncing updates if working with frequently changing selections