@dnd-grid/react
A drag-and-drop (DnD), resizable grid layout with responsive breakpoints for React.
Installation
npm install @dnd-grid/react
Usage
import { useState } from "react";
import { DndGrid, type Layout } from "@dnd-grid/react";
import "@dnd-grid/react/styles.css";
const layout: Layout = [
{ id: "a", x: 0, y: 0, w: 2, h: 2 },
{ id: "b", x: 2, y: 0, w: 2, h: 2 },
{ id: "c", x: 4, y: 0, w: 2, h: 2 },
];
function MyGrid() {
const [currentLayout, setLayout] = useState(layout);
return (
<DndGrid
layout={currentLayout}
cols={12}
rowHeight={30}
onLayoutChange={(newLayout) => setLayout(newLayout)}
>
<div key="a">A</div>
<div key="b">B</div>
<div key="c">C</div>
</DndGrid>
);
}
Headless usage (advanced)
DndGrid is the recommended default. If you need a custom wrapper or want to
control item rendering, use the headless useDndGrid hook and render
GridItem manually.
Docs: https://dnd-grid.com/docs/hooks/use-dnd-grid
Auto width by default
DndGrid and ResponsiveDndGrid measure container width with
ResizeObserver. Use containerProps to style the measurement wrapper, and
measureBeforeMount / initialWidth to control the first render.
import { DndGrid, type Layout } from "@dnd-grid/react";
const layout: Layout = [
{ id: "a", x: 0, y: 0, w: 2, h: 2 },
{ id: "b", x: 2, y: 0, w: 2, h: 2 },
];
function MyGrid() {
return (
<DndGrid
layout={layout}
cols={12}
rowHeight={30}
containerProps={{ className: "w-full", style: { maxWidth: 600 } }}
>
<div key="a">A</div>
<div key="b">B</div>
</DndGrid>
);
}
Explicit width (when you already have it)
If you already measure width (SSR or custom measurement), pass the width
directly:
import {
DndGrid,
type Layout,
useContainerWidth,
} from "@dnd-grid/react";
const layout: Layout = [
{ id: "a", x: 0, y: 0, w: 2, h: 2 },
{ id: "b", x: 2, y: 0, w: 2, h: 2 },
];
function MyGrid() {
const { width, containerRef, mounted } = useContainerWidth({
measureBeforeMount: true,
initialWidth: 600,
});
return (
<div ref={containerRef} className="w-full" style={{ maxWidth: 600 }}>
{mounted && width > 0 && (
<DndGrid
layout={layout}
cols={12}
rowHeight={30}
width={width}
>
<div key="a">A</div>
<div key="b">B</div>
</DndGrid>
)}
</div>
);
}
Features
- 100% React - no jQuery
- Compatible with server-rendered apps
- Drag-and-drop widgets
- Resizable widgets
- Auto-scroll near edges while dragging
- State-aware styling via data attributes and item hooks
- Static widgets
- Configurable packing: horizontal, vertical, or off
- Bounds checking for dragging and resizing
- Widgets may be added or removed without rebuilding grid
- Layout can be serialised and restored
- Grid items placed using CSS transforms
Props
children | ReactNode | required | Grid items; each child key must match a layout id. |
layout | Layout | [] | Array of layout items; missing entries are derived from children (dev warning). |
width | number | undefined | Optional container width in pixels. When provided, measurement is skipped. |
measureBeforeMount | boolean | true | Delay rendering until the container width is measured. |
initialWidth | number | 1280 | Width used before measurement when rendering early. |
containerProps | HTMLAttributes<HTMLDivElement> | undefined | Props applied to the measurement wrapper. |
cols | number | 12 | Number of columns |
rowHeight | number | 150 | Height of a row in pixels |
autoSize | boolean | true | Auto-size the container height |
maxRows | number | Infinity | Maximum number of rows |
gap | number | { top: number; right: number; bottom: number; left: number } | 10 | Gap around items |
containerPadding | number | { top: number; right: number; bottom: number; left: number } | null | null | Container padding (null uses gap) |
className | string | "" | Extra class for the grid container |
style | CSSProperties | {} | Inline styles for the grid container |
draggable | boolean | true | Enable dragging |
resizable | boolean | true | Enable resizing |
autoScroll | boolean | AutoScrollOptions | true | Auto-scroll when dragging near scroll edges |
bounded | boolean | false | Keep items within container bounds |
compactor | Compactor | verticalCompactor | Compaction strategy |
constraints | LayoutConstraint[] | defaultConstraints | Constraints applied during drag/resize |
validation | boolean | true in dev, false in prod | Validate layouts at runtime with Zod |
callbackThrottle | number | { drag?: number; resize?: number } | undefined | Throttle onDrag and onResize callbacks |
animationConfig | AnimationConfig | default config | Configure spring and shadow animations |
reducedMotion | ReducedMotionSetting | boolean | "system" | Motion preference override |
resizeHandles | ResizeHandleAxis[] | ["se"] | Resize handle positions |
resizeHandle | ReactElement | ((axis, ref) => ReactElement) | undefined | Custom resize handle component |
transformScale | number | 1 | Scale factor for CSS transforms |
dragTouchDelayDuration | number | 250 | Touch delay before drag starts (ms) |
dragHandle | string | "" | Selector for drag handles |
dragCancel | string | "" | Selector for elements that cancel drag |
droppingItem | Partial<LayoutItem> | { id: "__dropping-elem__", w: 1, h: 1 } | Defaults for the dropping placeholder |
slotProps | SlotProps | undefined | Slot styling for items, placeholders, and handles |
liveAnnouncements | LiveAnnouncementsOptions | false | enabled | Configure aria-live announcements or disable |
innerRef | Ref<HTMLDivElement> | undefined | Ref to the container element |
aria-label | string | undefined | Accessible label for the grid |
aria-labelledby | string | undefined | ID(s) of labelling elements |
aria-describedby | string | undefined | ID(s) of descriptive elements |
Layout is provided via the layout prop; data-grid on children is not supported.
Missing layout entries are derived from children (with a dev warning).
Drop behavior is enabled when you provide onDrop or onDropDragOver.
DndGrid renders role="grid" and assigns role="gridcell" plus row/column indices to items. Placeholders and static items are excluded from set indexing.
Responsive layouts
Use ResponsiveDndGrid for breakpoint-aware layouts:
import { ResponsiveDndGrid, type ResponsiveLayouts } from "@dnd-grid/react";
const layouts: ResponsiveLayouts = {
lg: [{ id: "a", x: 0, y: 0, w: 3, h: 2 }],
md: [{ id: "a", x: 0, y: 0, w: 4, h: 2 }],
};
function ResponsiveGrid() {
return (
<ResponsiveDndGrid
layouts={layouts}
gap={{ lg: 16, md: { top: 12, right: 16, bottom: 12, left: 16 } }}
containerPadding={16}
>
<div key="a">A</div>
<div key="b">B</div>
</ResponsiveDndGrid>
);
}
For custom wrappers or manual width control, compose
useDndGridResponsiveLayout with DndGrid.
Layout item
Each item in the layout array has these properties:
interface LayoutItem {
id: string;
x: number;
y: number;
w: number;
h: number;
minW?: number;
maxW?: number;
minH?: number;
maxH?: number;
data?: TData;
resizeHandles?: ResizeHandleAxis[];
constraints?: LayoutConstraint[];
static?: boolean;
draggable?: boolean;
resizable?: boolean;
bounded?: boolean;
}
Styles
The default stylesheet ships as layout + theme layers. Import the combined file, or split them as needed:
import "@dnd-grid/react/styles.css";
import "@dnd-grid/react/base.css";
import "@dnd-grid/react/theme.css";
Theme variables are exposed as CSS custom properties:
:root {
--dnd-grid-scale: 1;
--dnd-grid-radius: 24px;
--dnd-grid-placeholder-bg: rgba(0, 0, 0, 0.012);
--dnd-grid-placeholder-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06);
--dnd-grid-handle-size: 16px;
--dnd-grid-handle-bg: rgb(255, 255, 255);
--dnd-grid-handle-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.06),
0 9px 9px rgba(0, 0, 0, 0.04),
0 5.83333px 5.27083px rgba(0, 0, 0, 0.03),
0 3.46667px 2.86667px rgba(0, 0, 0, 0.024),
0 1.8px 1.4625px rgba(0, 0, 0, 0.02),
0 0.733333px 0.733333px rgba(0, 0, 0, 0.016);
--dnd-grid-transition-duration: 200ms;
--dnd-grid-transition-easing: cubic-bezier(0.2, 0, 0, 1);
}
Compactors
Compactors control how items are packed after drag/resize. You can use the built-ins or provide your own:
import {
verticalCompactor,
horizontalCompactor,
noCompactor,
fastVerticalCompactor,
} from "@dnd-grid/react";
<DndGrid compactor={verticalCompactor} />;
<DndGrid compactor={horizontalCompactor} />;
<DndGrid compactor={noCompactor} />;
<DndGrid compactor={fastVerticalCompactor} />;
Constraints
Constraints let you enforce rules during drag/resize:
import { defaultConstraints, aspectRatio, gridBounds } from "@dnd-grid/react";
<DndGrid constraints={[gridBounds, aspectRatio(16 / 9)]} />;
Item state hook
Read the layout item and interaction state from within a child component:
import { useDndGridItemState } from "@dnd-grid/react";
function Card() {
const { item, state } = useDndGridItemState();
return (
<div className={state.resizing ? "is-resizing" : ""}>
Item {item.id}
</div>
);
}
The hook must be used inside a DndGrid item (it throws if rendered elsewhere).
External drag sources
If you manage drag state outside of the grid (for example with @dnd-kit), use
the grid handle to drive the dropping placeholder and commit the drop:
const gridRef = useRef<DndGridHandle>(null);
const handleMove = (clientX: number, clientY: number) => {
gridRef.current?.handleExternalDrag({ clientX, clientY });
};
const handleDrop = (clientX: number, clientY: number, event?: Event) => {
gridRef.current?.handleExternalDrag({
clientX,
clientY,
event,
type: "drop",
});
};
const handleCancel = (clientX: number, clientY: number, event?: Event) => {
gridRef.current?.handleExternalDrag({
clientX,
clientY,
event,
type: "cancel",
});
};
Use onDropDragOver to override the dropping placeholder size while dragging.
If your drag library exposes a native event, pass it via event instead of
generating one.
Callbacks
onLayoutChange | (layout: Layout) => void | Called when layout changes |
onDragStart | (event: GridDragEvent) => void | Called when drag starts |
onDrag | (event: GridDragEvent) => void | Called during drag |
onDragEnd | (event: GridDragEvent) => void | Called when drag ends |
onResizeStart | (event: GridResizeEvent) => void | Called when resize starts |
onResize | (event: GridResizeEvent) => void | Called during resize |
onResizeEnd | (event: GridResizeEvent) => void | Called when resize ends |
onDrop | (layout, item, e) => void | Called when item is dropped |
onDropDragOver | (e) => { w?: number; h?: number } | false | Customise dropping item |
CSS classes
Common CSS classes include:
.dnd-grid - Grid container
.dnd-grid-item - Grid item
.dnd-grid-item-content - Non-placeholder grid item
.dnd-grid-placeholder - Placeholder during drag/resize
.dnd-draggable - Draggable item
.dnd-draggable-dragging - Item being dragged
.resizing - Item currently being resized
.static - Static (locked) item
.dnd-grid-animating - Item settling after drag
.dropping - Dropping placeholder item
.dnd-grid-resize-handle - Resize handle element
body.dnd-grid-dragging - Global dragging state
body.dnd-grid-resizing - Global resizing state
Data attributes
The grid sets data-* attributes for stateful styling:
data-dnd-grid on the container
data-dnd-grid-live-region on the aria-live element
data-dnd-grid-item on each item
data-dnd-grid-item-id on each item
data-dnd-grid-handle and data-handle-axis on resize handles
data-dragging, data-resizing, data-settling, data-disabled, data-draggable, data-resizable on items when true
[data-dnd-grid-item][data-resizing] {
z-index: 10;
}
[data-dnd-grid-handle][data-handle-axis="sw"] {
cursor: sw-resize;
}
Acknowledgements
This project is based on react-grid-layout by Samuel Reed (STRML). The original project provided the foundation for the grid layout algorithms and core functionality.
Licence
MIT Licence - Copyright (c) 2025 Matthew Blode