rfcs/0002-pluggable-constraints.md
Add a LayoutConstraint interface to react-grid-layout v2, following the same patterns as Compactor and PositionStrategy. This enables pluggable position and size constraints, replacing hardcoded logic with composable, tree-shakeable constraint functions.
See these examples to understand the constraint system in action:
Run the examples locally with yarn dev and navigate to the constraints examples.
Currently, constraints are hardcoded in multiple places:
calcXY: clamps x/y to grid boundscalcWH: clamps w/h to grid bounds, with special handle logicGridItem.tsx: applies minW/maxW/minH/maxHposition.ts: pixel-level container boundslayout.ts: correctBounds overflow handlingUsers requesting features like:
...cannot implement them without forking the library.
interface LayoutConstraint {
/** Constraint identifier for debugging */
readonly name: string;
/**
* Constrain position during drag operations.
* Called after grid unit conversion, before layout update.
*/
constrainPosition?(
item: LayoutItem,
x: number,
y: number,
context: ConstraintContext
): { x: number; y: number };
/**
* Constrain size during resize operations.
* Called after grid unit conversion, before layout update.
*/
constrainSize?(
item: LayoutItem,
w: number,
h: number,
handle: ResizeHandleAxis,
context: ConstraintContext
): { w: number; h: number };
}
interface ConstraintContext {
cols: number;
maxRows: number;
containerWidth: number;
containerHeight: number;
rowHeight: number;
margin: readonly [number, number];
layout: Layout;
}
// Grid boundary constraints (enabled by default)
// Keeps items within 0 to cols (x) and 0 to maxRows (y)
export const gridBounds: LayoutConstraint;
// Container bounding (opt-in, replaces isBounded)
// Uses containerHeight to calculate visible rows
export const containerBounds: LayoutConstraint;
// Axis-specific bounding
export const boundedX: LayoutConstraint; // Only constrains X
export const boundedY: LayoutConstraint; // Only constrains Y
// Item min/max constraints (enabled by default)
// Enforces per-item minW/maxW/minH/maxH properties
export const minMaxSize: LayoutConstraint;
// Aspect ratio constraint (pixel-aware)
// Maintains width:height ratio in actual pixels, accounting for
// different column widths vs row heights
export function aspectRatio(ratio: number): LayoutConstraint;
// Snap-to-grid constraint
// Snaps positions to multiples of step values
export function snapToGrid(stepX: number, stepY?: number): LayoutConstraint;
// Grid-wide min/max size
export function minSize(minW: number, minH: number): LayoutConstraint;
export function maxSize(maxW: number, maxH: number): LayoutConstraint;
Apply constraints to all items in the grid:
import {
GridLayout,
gridBounds,
minMaxSize,
aspectRatio
} from "react-grid-layout";
// Default behavior
<GridLayout constraints={[gridBounds, minMaxSize]} />
// Add aspect ratio to all items
<GridLayout constraints={[gridBounds, minMaxSize, aspectRatio(16/9)]} />
// No constraints (items can be positioned/sized freely)
<GridLayout constraints={[]} />
Apply constraints to specific items via the layout:
const layout = [
// Video player with 16:9 aspect ratio
{ i: "video", x: 0, y: 0, w: 4, h: 2, constraints: [aspectRatio(16 / 9)] },
// Sidebar that can only move horizontally
{ i: "sidebar", x: 4, y: 0, w: 2, h: 4, constraints: [boundedX] }
];
// Custom constraint: items can only be placed in even columns
const evenColumnsOnly: LayoutConstraint = {
name: "evenColumnsOnly",
constrainPosition(item, x, y, context) {
const evenX = Math.round(x / 2) * 2;
return { x: evenX, y };
}
};
// Custom constraint: maximum area
const maxArea = (area: number): LayoutConstraint => ({
name: `maxArea(${area})`,
constrainSize(item, w, h, handle, context) {
const currentArea = w * h;
if (currentArea <= area) return { w, h };
// Reduce the dimension being resized
if (handle.includes("e") || handle.includes("w")) {
return { w: Math.floor(area / h), h };
}
return { w, h: Math.floor(area / w) };
}
});
See Example 21 for more custom constraint examples.
export const defaultConstraints = [gridBounds, minMaxSize];
When no constraints prop is provided, defaultConstraints is used, maintaining backwards compatibility.
Constraints are applied in array order, allowing composition:
// Order matters!
// gridBounds runs first, then minMaxSize
<GridLayout constraints={[gridBounds, minMaxSize]} />
// If you want boundedX instead of full grid bounds,
// use boundedX as the position constraint
<GridLayout constraints={[boundedX, minMaxSize]} />
Constraints control where items can be positioned during drag/resize operations.
Compaction runs AFTER drag/resize and can move items to fill gaps.
With vertical compaction (default), items float up after being dropped. This can make position constraints like boundedX (free Y movement) less visible because compaction moves items back up.
To see position constraints clearly, use noCompactor:
import {
GridLayout,
noCompactor,
boundedX,
minMaxSize
} from "react-grid-layout";
<GridLayout constraints={[boundedX, minMaxSize]} compactor={noCompactor} />;
See Example 19 which includes a "No Compaction" toggle.
// Apply position constraints
function applyPositionConstraints(
constraints: LayoutConstraint[],
item: LayoutItem,
x: number,
y: number,
context: ConstraintContext
): { x: number; y: number };
// Apply size constraints
function applySizeConstraints(
constraints: LayoutConstraint[],
item: LayoutItem,
w: number,
h: number,
handle: ResizeHandleAxis,
context: ConstraintContext
): { w: number; h: number };
In GridItem.tsx:
In GridLayout.tsx:
constraints prop accepts LayoutConstraint[]defaultConstraintsprocessGridItemconstraints propdefaultConstraints maintains current behaviorisBounded prop maps to adding containerBounds constraintminW/maxW/minH/maxH continue to work via minMaxSize constraintConstraintContext.layout for collision-aware constraints