react-grid-layout
react-grid-layout is a React dashboard layout engine for draggable and resizable rectangular widgets. You describe each widget in grid units with an id, column and row position, width, height, and optional constraints; the component converts that model to pixels and resolves collisions. It supports fixed and responsive grids, static items, horizontal or vertical compaction, free positioning, external drops, scaled containers, serialization, and lower-level hooks. Version 2 is a TypeScript rewrite with a new grouped configuration API and a legacy entry point for version 1 applications.
The strongest general-purpose choice for a React dashboard whose layout is genuinely user-editable, and version 2 finally brings first-party types and modular internals. Do not install it for a normal responsive page, and budget real work for persistence, migration, SSR sizing, and keyboard controls.
Use it if
- You are building an editable dashboard whose widgets need both drag-to-reorder and resize handles
- You need separate saved layouts for desktop, tablet, and phone breakpoints
- Your layout must combine movable widgets with fixed items and minimum or maximum size constraints
- You need React components plus reusable collision, compaction, and positioning algorithms from one package
- You only need a responsive page grid: CSS Grid handles ordinary layout without 22.8 KB gzipped of JavaScript and six runtime dependencies
- You need a masonry feed driven by content height: every layout item here uses explicit x, y, w, and h grid units rather than measuring arbitrary content into a masonry flow
- You expect layouts to persist automatically: the README says layouts can be serialized, but saving, schema migration, user ownership, conflict handling, and server storage remain your application work
- You cannot budget for a version 1 migration: version 2 requires width, moves data-grid to the legacy wrapper, makes callback values immutable, removes the UMD build, and replaces flat configuration props
- You need keyboard-first editing out of the box: the documented interaction API centers on pointer drag and resize callbacks, so accessible move and resize controls must be designed separately
Setup reality
Install react-grid-layout with React and ReactDOM, then import both react-grid-layout/css/styles.css and react-resizable/css/styles.css. Missing the second stylesheet is the classic first-run surprise: resize handles exist but look absent or wrong. Version 2.2.4 includes TypeScript types and ESM and CommonJS exports, and its README compatibility table targets React 18 and newer even though the package metadata still declares peers from React 16.3. The v2 component requires a numeric width. The recommended useContainerWidth hook gives you width, containerRef, and mounted; attach the ref to the wrapper and avoid rendering the grid until mounted, or provide a trusted initial width. For SSR, use measureBeforeMount and an initialWidth to limit hydration movement. Layout ids must match the React child keys, positions and sizes are grid units rather than pixels, and every breakpoint needs a compatible column count. The component reports layout changes but does not store them, so persist immutable copies and validate old saved layouts when widgets or constraints change. Importing from react-grid-layout uses the grouped v2 props such as gridConfig, dragConfig, and resizeConfig. Existing v1 code should temporarily import from react-grid-layout/legacy; data-grid on children, WidthProvider, and old flat props live there. The migration is not cosmetic: width became required, drag-start waits for three pixels of movement, callback parameters are read-only, verticalCompact was removed, and the UMD build disappeared. Transformed parent containers also need createScaledStrategy with the actual scale or pointer coordinates drift. Finally, dragging is not a complete accessibility design. Add explicit keyboard move and resize actions, announce changes, and persist only after a completed interaction rather than writing on every pointer movement.
Patterns
Create a measured version 2 gridcreate-basic-grid
import ReactGridLayout, { useContainerWidth } from 'react-grid-layout';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
const layout = [
{ i: 'chart', x: 0, y: 0, w: 6, h: 3 },
{ i: 'totals', x: 6, y: 0, w: 3, h: 2 },
];
export function Dashboard() {
const { width, containerRef, mounted } = useContainerWidth();
return <div ref={containerRef}>
{mounted && <ReactGridLayout width={width} layout={layout} gridConfig={{ cols: 12, rowHeight: 40 }}>
<section key="chart">Chart</section>
<section key="totals">Totals</section>
</ReactGridLayout>}
</div>;
}Import both stylesheets. Every layout i must equal its child's React key, and version 2 requires an explicit measured width.
Provide layouts for multiple breakpointsbuild-responsive-grid
import { Responsive, useContainerWidth } from 'react-grid-layout';
const layouts = {
lg: [{ i: 'sales', x: 0, y: 0, w: 6, h: 3 }],
sm: [{ i: 'sales', x: 0, y: 0, w: 6, h: 3 }],
};
function ResponsiveDashboard() {
const { width, containerRef, mounted } = useContainerWidth();
return <div ref={containerRef}>
{mounted && <Responsive width={width} layouts={layouts}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}>
<div key="sales">Sales</div>
</Responsive>}
</div>;
}Store all breakpoint layouts, not only the active one. An item width must fit the column count at each breakpoint where it appears.
Save immutable layout changespersist-layout
function Dashboard() {
const [layout, setLayout] = useState(loadLayout);
const handleLayoutChange = (nextLayout) => {
const saved = nextLayout.map((item) => ({ ...item }));
setLayout(saved);
localStorage.setItem('dashboard-layout-v2', JSON.stringify(saved));
};
return <ReactGridLayout width={width} layout={layout} onLayoutChange={handleLayoutChange}>...</ReactGridLayout>;
}Version 2 callback parameters are read-only. Copy them instead of mutating, and version stored data so future widget and constraint changes can be migrated.
Pin one widget in placemake-static-widget
const layout = [
{ i: 'notice', x: 0, y: 0, w: 12, h: 1, static: true },
{ i: 'chart', x: 0, y: 1, w: 6, h: 4 },
];A static item cannot be dragged or resized and also participates in collision handling, so movable items route around it.
Set minimum and maximum widget dimensionsconstrain-widget-size
const layout = [
{
i: 'chart', x: 0, y: 0, w: 6, h: 4,
minW: 4, maxW: 10, minH: 3, maxH: 8,
},
];Constraints use grid units, not pixels. Keep minW at or below the smallest breakpoint's column count.
Limit dragging to a card handleconfigure-drag-handle
<ReactGridLayout
width={width}
layout={layout}
dragConfig={{ enabled: true, handle: '.widget-handle', cancel: 'button,input,a' }}
>
<article key="orders">
<header className="widget-handle">Orders</header>
<button>Refresh</button>
</article>
</ReactGridLayout>Use a cancel selector for interactive controls inside draggable cards. Version 2 starts a drag only after the pointer moves three pixels by default.
Expose more resize directionsconfigure-resize-handles
<ReactGridLayout
width={width}
layout={layout}
resizeConfig={{
enabled: true,
handles: ['s', 'e', 'se', 'w'],
}}
>
{children}
</ReactGridLayout>The default is only the southeast handle. The react-resizable stylesheet is required for visible, correctly positioned handles.
Preserve empty space in a free-positioned griddisable-compaction
import ReactGridLayout from 'react-grid-layout';
import { noCompactor } from 'react-grid-layout/core';
<ReactGridLayout width={width} layout={layout} compactor={noCompactor}>
{children}
</ReactGridLayout>Without compaction, gaps remain where users leave them. Collision behavior is separate, so choose prevent-collision or overlap semantics deliberately.
Pack widgets toward the leftcompact-horizontally
import { horizontalCompactor } from 'react-grid-layout';
<ReactGridLayout
width={width}
layout={layout}
compactor={horizontalCompactor}
>
{children}
</ReactGridLayout>The default compactor is vertical. Horizontal compaction changes how collision resolution moves neighboring items, so test saved layouts before switching.
Correct pointer math inside a scaled containersupport-scaled-container
import ReactGridLayout from 'react-grid-layout';
import { createScaledStrategy } from 'react-grid-layout/core';
<div style={{ transform: 'scale(0.75)', transformOrigin: 'top left' }}>
<ReactGridLayout
width={width}
layout={layout}
positionStrategy={createScaledStrategy(0.75)}
>
{children}
</ReactGridLayout>
</div>The scale passed to createScaledStrategy must match the ancestor transform or dragging and resizing will drift away from the pointer.
Measure before showing an SSR gridprepare-for-ssr
function Dashboard() {
const { width, containerRef, mounted } = useContainerWidth({
measureBeforeMount: true,
initialWidth: 1200,
});
return <div ref={containerRef}>
{mounted ? <ReactGridLayout width={width} layout={layout}>{children}</ReactGridLayout> : null}
</div>;
}Choose an initialWidth close to the server-rendered container. Delaying until mounted avoids wrong geometry but can leave a visible blank area before hydration.
Keep the version 1 API during migrationmigrate-version-one
import GridLayout, { Responsive, WidthProvider } from 'react-grid-layout/legacy';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
const AutoWidthGrid = WidthProvider(GridLayout);
<AutoWidthGrid cols={12} rowHeight={30}>
<div key="a" data-grid={{ x: 0, y: 0, w: 4, h: 2 }}>A</div>
</AutoWidthGrid>The legacy subpath provides version 1 runtime compatibility. New version 2 code should use an explicit layout, useContainerWidth, and grouped configuration props.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| gridstack | npm | Choose it for a framework-independent dashboard engine with official wrappers and no runtime dependencies |
| react-mosaic-component | npm | Choose it for IDE-style split panes and tiling windows rather than a free two-dimensional dashboard grid |
| @dnd-kit/core | npm | Choose it when you need accessible custom drag-and-drop behavior but want to own all grid placement and resizing logic |
| golden-layout | npm | Choose it for dockable, nested, multi-window application panels instead of row-and-column dashboard cards |