npm.io
0.3.0 • Published 1 week ago

@wordpress/grid

Licence
GPL-2.0-or-later
Version
0.3.0
Deps
7
Size
1023 kB
Vulns
0
Weekly
0
Stars
11.7K

Grid

This package is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.

A collection of grid layout components for arranging tiles in dashboard-style surfaces.

This package exposes two components, each implementing a different layout model:

  • DashboardGrid is a 2D packed grid: tiles declare explicit (width, height) spans in column/row units and can span multiple columns and rows.
  • DashboardLanes is a masonry-style surface aligned with the emerging WebKit spec display: grid-lanes. Tiles declare a column span only; heights are driven by content; placement follows a source-ordered, shortest-lane skyline with a flow-tolerance tiebreaker.

Installation

Install the module:

npm install @wordpress/grid --save

This package assumes that your code will run in an ES2015+ environment. If you're using an environment that has limited or no support for such language features and APIs, you should include the polyfill shipped in @wordpress/babel-preset-default in your code.

Setup

Component styles are CSS Modules injected at runtime when a component mounts; there is no stylesheet to enqueue or import.

Visual defaults (tile gap, elevation, motion, placeholder strokes) read the design tokens that @wordpress/theme publishes as --wpds-* CSS custom properties.

Within standard WordPress editor screens

In standard WordPress editor screens (such as the post editor or the site editor), the design tokens stylesheet is managed centrally by Gutenberg. You don't need to add any setup yourself.

Elsewhere

Install and load the design tokens stylesheet in your application:

npm install @wordpress/theme
import '@wordpress/theme/design-tokens.css';

Without the tokens the components stay functional, but gaps, elevations, and interaction visuals lose their intended values. Alternatively, define the --wpds-* custom properties the package consumes yourself.

Choosing a component

Need Use
Fixed-cell tile dashboard, content fills its cell. DashboardGrid
Masonry / waterfall surface, content drives height. DashboardLanes
Static layout primitive (no per-item state, no drag). __experimentalGrid from @wordpress/components

Both components here are higher-level: the user (not the developer) places and resizes tiles, and the result is emitted through onChangeLayout. For a static CSS Grid with no spans, drag, or per-item state, use __experimentalGrid from @wordpress/components.


DashboardGrid

A 2D packed grid where each child has an explicit column and row span.

Usage
import { DashboardGrid } from '@wordpress/grid';

const layout = [
	{ key: 'a', width: 2, height: 2 },
	{ key: 'b', width: 4, height: 1 },
	{ key: 'c', width: 'fill', height: 1 },
	{ key: 'd', width: 'full', height: 1 },
];

function Dashboard() {
	const [ current, setCurrent ] = useState( layout );

	return (
		<DashboardGrid
			layout={ current }
			columns={ 6 }
			editMode
			onChangeLayout={ setCurrent }
		>
			<div key="a">Tile A</div>
			<div key="b">Tile B</div>
			<div key="c">Tile C</div>
			<div key="d">Tile D</div>
		</DashboardGrid>
	);
}

Each child must have a key prop that matches an entry in the layout array. Children without a matching layout entry render at the end of the grid without explicit placement and fall through CSS Grid's auto-flow.

Layout model
interface DashboardGridLayoutItem {
	key: string; // matches child key
	width?: number | 'fill' | 'full'; // column span (see below)
	height?: number; // rows to span
	order?: number; // lower values render first (responsive mode)
}

width is a discriminated value:

  • number: span that many columns (clamped to the grid's column count).
  • 'fill': fill the remaining columns in the current row.
  • 'full': span every column (grid-column: 1 / -1), forcing a row break.

'fill' is resolved per-row against the remaining free space.

Props
Prop Type Default Description
layout DashboardGridLayoutItem[] Required. Positions and sizes keyed by child key.
children ReactNode Required. Each child needs a key matching a layout entry.
columns number 6 Total columns (fixed mode).
minColumnWidth number If set, enables responsive mode: columns derived from container width. Mutually exclusive with columns.
rowHeight number | 'auto' 'auto' Row height in pixels, or 'auto' to let content size rows.
editMode boolean false Enables drag-to-reorder and resize handles.
onChangeLayout ( layout ) => void Fired when the user commits a drag or resize.
onPreviewLayout ( layout ) => void Fired continuously during a drag or resize with the in-progress layout. Use for live feedback; onChangeLayout still emits the committed result.
renderResizeHandle ComponentType< ResizeHandleRenderProps > Override the default corner-triangle resize handle. See Custom resize handle.
renderDragPreview ComponentType< DragPreviewRenderProps > Wrap the dragged-clone visual mounted inside <DragOverlay>. See Custom drag preview.
renderGridOverlay ComponentType< GridOverlayRenderProps > Override the default edit-mode overlay that visualizes the column and row tracks. Receives the resolved columns, rows, rowHeight, and isActive.
className string Extra class on the grid root.
style CSSProperties Inline styles on the grid root; the grid's own layout styles win over them.

DashboardGrid forwards refs to its root <div>, and standard <div> attributes (id, aria-*, data-*, event handlers, style, etc.) flow through. The grid's own layout styles (gridTemplateColumns, gridAutoRows) override any user-supplied style for those properties. Tile gap is owned by a design-system token; override it with --wp-grid-gap (see Theming with CSS variables).

Child-level props

Children render with the layout entry that matches their key. An optional prop read off the child lets you keep controls interactive while edit mode is on:

Child prop Type Description
actionableArea ReactNode Content rendered above the draggable surface of the grid item. Useful for close buttons, menus, or links that must stay clickable in edit mode.
Modes
Fixed columns
<DashboardGrid layout={ layout } columns={ 12 }>
	{ children }
</DashboardGrid>
Responsive

Columns are computed from container width using minColumnWidth as the lower bound per column. A ResizeObserver recomputes on container resize.

<DashboardGrid layout={ layout } minColumnWidth={ 240 }>
	{ children }
</DashboardGrid>

In responsive mode, layout items can provide an order to control display order independently of array position.

Edit mode

When editMode is true:

  • Items become draggable (powered by @dnd-kit). The original tile stays in place as a dashed placeholder while a clone follows the cursor through <DragOverlay>.
  • A resize handle appears on the bottom-right of each item. A solid outline previews the target size as the cursor moves.
  • While any tile is dragging or resizing, actionableArea content on every tile is set inert so hovers on other tiles can't steal the gesture.
  • onChangeLayout fires after drop or resize with the new layout.
  • onPreviewLayout fires continuously during the interaction for live feedback; the committed layout is still emitted via onChangeLayout.
  • Sibling tiles animate into their new positions when the layout reflows.

DashboardLanes

A masonry-style surface aligned with display: grid-lanes. Items declare a column span; heights are driven by content; placement follows a source-ordered, shortest-lane skyline with a flow-tolerance tiebreaker.

The layout model and the placement algorithm are described in Introducing CSS Grid Lanes on the WebKit blog. This package implements the same model in JavaScript so it works today on browsers that do not yet support display: grid-lanes natively; the skyline + tolerance core is adapted from Simon Willison's CSS Grid Lanes Polyfill (MIT). Once native support lands across browsers, the polyfill can be removed without any public API change.

Usage
import { DashboardLanes } from '@wordpress/grid';

const layout = [
	{ key: 'a' },
	{ key: 'hero', width: 2 },
	{ key: 'b' },
	{ key: 'c' },
];

function Pinboard() {
	const [ current, setCurrent ] = useState( layout );

	return (
		<DashboardLanes
			layout={ current }
			columns={ 4 }
			editMode
			onChangeLayout={ setCurrent }
		>
			<Tile key="a">A</Tile>
			<Tile key="hero">Hero (spans 2 lanes)</Tile>
			<Tile key="b">B</Tile>
			<Tile key="c">C</Tile>
		</DashboardLanes>
	);
}

Each child must have a key prop that matches an entry in the layout array. Children without a matching layout entry render at the end of the surface without explicit placement and fall through the lanes auto-flow.

Layout model
interface DashboardLanesLayoutItem {
	key: string; // matches child key
	width?: number; // lanes to span (default 1)
	lane?: number; // 0-indexed: pin to a specific lane
	order?: number; // lower values render first
}

There is no height field: lanes pack tiles vertically using each tile's intrinsic content height.

There is no 'fill': with auto-placement, no item is "left over" in a row; the algorithm always finds a lane.

'full' (span the entire surface width) is expressed by setting width to the lane count.

To anchor a tile to a specific column, set lane to its 0-indexed position. Pinned tiles are placed before auto-placed ones, so auto items flow around them; out-of-range values (negative, or beyond columns - width) are clamped.

Props
Prop Type Default Description
layout DashboardLanesLayoutItem[] Required. Span and order keyed by child key.
children ReactNode Required. Each child needs a key matching a layout entry.
columns number 6 Total lanes (fixed mode).
minColumnWidth number If set, enables responsive mode: lane count derived from container width. Mutually exclusive with columns.
flowTolerance number 16 Pixel tolerance for source-order tiebreaking when two candidate lanes have similar baselines. Larger values keep tiles closer to reading order at the cost of bigger empty regions.
rowUnit number 4 Snap unit for the polyfill's grid-row-start math. Smaller values produce sharper placement at the cost of a larger implicit row count. Ignored on browsers with native display: grid-lanes support.
editMode boolean false Enables drag-to-reorder and horizontal resize.
onChangeLayout ( layout ) => void Fired when the user commits a drag or resize.
onPreviewLayout ( layout ) => void Fired continuously during a drag or resize.
renderResizeHandle ComponentType< ResizeHandleRenderProps > Override the default side-grip resize handle. See Custom resize handle.
renderDragPreview ComponentType< DragPreviewRenderProps > Wrap the dragged-clone visual mounted inside <DragOverlay>. See Custom drag preview.
renderGridOverlay ComponentType< GridOverlayRenderProps > Override the default edit-mode overlay that visualizes the lane tracks. Receives the resolved columns and isActive; lanes pass no row metrics because heights are content-driven.
className string Extra class on the surface root.
style CSSProperties Inline styles on the surface root; the surface's own layout styles win over them.
Native vs polyfill

DashboardLanes checks CSS.supports( 'display', 'grid-lanes' ) once at mount.

  • When supported (Safari 26+, others as the spec ships), the component emits display: grid-lanes and the spec's CSS, and lets the engine handle layout. The placement layer mounts no per-tile observers; the only ResizeObserver left is the container-width one used for responsive mode and resize-step math.
  • When unsupported, an internal hook (useLanePlacement) measures each tile's height with a ResizeObserver, runs the source-ordered shortest-lane algorithm, and emits explicit grid-column-start and grid-row-start / grid-row-end: span N values on each tile.

The same DOM contract is preserved in both paths; the visual is the same.

Edit mode

Drag-to-reorder works the same as in DashboardGrid. Resize is horizontal-only: tile heights are content-driven, so there is no vertical resize gesture. The default handle is a vertical bar centered on the trailing edge; the cursor is ew-resize.

Sibling tiles animate into their new positions when the layout reflows.


Shared topics

Performance

onPreviewLayout re-renders the parent on every gesture frame. To keep the components' internal children walk from re-running each frame, memoize the children array when its content is stable:

const tiles = useMemo(
	() => layout.map( ( item ) => <Tile key={ item.key }>...</Tile> ),
	[ layout ]
);

return (
	<DashboardLanes layout={ layout } editMode onPreviewLayout={ ... }>
		{ tiles }
	</DashboardLanes>
);

Without it the surface still works but walks the children on every preview update; the overhead is minor up to ~50 tiles and grows from there. For DashboardLanes, placement runs in a useLayoutEffect throttled to one frame per measurement burst.

Accessibility

Drag-to-reorder is operable from the keyboard via @dnd-kit's keyboard sensor:

  • Tab to focus a tile.
  • Space to pick it up.
  • Arrow keys to move it between positions.
  • Space to drop, or Escape to cancel.

Resize handles are currently pointer-only.

Custom resize handle

Both components accept a renderResizeHandle prop to override the default visual. The surface owns the resize math (column/row stepping, throttled delta loop, layout commit) and passes the gesture wiring (ref, listeners, attributes) as props for the consumer to spread on the element that should receive pointer events. The dnd-kit <DndContext> for the resize gesture is internal to the handle wrapper; consumers do not need to mount their own.

import { Icon } from '@wordpress/ui';
import { resizeCornerNE } from '@wordpress/icons';

function CustomResizeHandle( {
	ref,
	listeners,
	attributes,
	verticalResizable,
} ) {
	return (
		<div
			ref={ ref }
			{ ...listeners }
			{ ...attributes }
			style={ {
				position: 'absolute',
				bottom: 4,
				insetInlineEnd: 4,
				cursor: verticalResizable ? 'nwse-resize' : 'ew-resize',
			} }
		>
			<Icon icon={ resizeCornerNE } size={ 16 } />
		</div>
	);
}

<DashboardGrid
	layout={ layout }
	editMode
	renderResizeHandle={ CustomResizeHandle }
>
	{ tiles }
</DashboardGrid>;

The component receives:

Prop Type Description
ref ( node ) => void dnd-kit ref; assign on the gesture-bearing element.
listeners SyntheticListenerMap | undefined Pointer/keyboard listeners; spread on the same element.
attributes DraggableAttributes Accessibility/dnd-kit attributes; spread alongside listeners.
verticalResizable boolean False on DashboardLanes and on DashboardGrid with rowHeight: 'auto'. Useful for adapting cursor or visual cue.
isResizing boolean True while the user is actively dragging this handle. Use it to swap colors, icons, or transforms during the gesture.
itemId string Owning tile's key.

The handle is only mounted while the surface is in edit mode (editMode={ true }), so the custom component never has to short-circuit on a disabled state.

Custom drag preview

While a tile is being dragged, dnd-kit clones it into a <DragOverlay> that follows the cursor. Both surfaces wrap that clone with a thin functional frame (scale, cursor: grabbing, pointer-events: none) that advertises the lift, but they do not impose visual chrome on top: any styles the consumer applied to the tile children carry through to the dragged clone unchanged.

When the dragged state should look structurally different from the persistent tile (a stronger shadow, a different border, an extra badge…), pass a renderDragPreview component. The surface mounts it inside the functional frame and supplies the cloned children plus the active tile's key:

import { DashboardGrid } from '@wordpress/grid';
import type { DragPreviewRenderProps } from '@wordpress/grid';

function DragPreview( { children }: DragPreviewRenderProps ) {
	return (
		<div className="my-tile-while-dragging">
			{ children }
		</div>
	);
}

<DashboardGrid
	layout={ layout }
	editMode
	renderDragPreview={ DragPreview }
>
	{ tiles }
</DashboardGrid>;

The component receives:

Prop Type Description
children ReactNode The cloned tile content the surface mounts inside <DragOverlay>. Place it where the visual wrapper expects the tile body.
itemId string Owning tile's key. Useful when chrome varies by tile.

For token-only tweaks (lift scale, placeholder opacity, outline color, placeholder radius), prefer the CSS variables below; reach for renderDragPreview only when the dragged state needs markup the persistent tile does not have.

Theming with CSS variables

Both surfaces expose a small set of CSS custom properties for visuals that need to flex between consumers without writing a render prop. Override them on any ancestor of the surface root (or on the root itself via style). All values fall back to sensible defaults.

Variable Default Applies to
--wp-grid-gap var(--wpds-dimension-gap-xl) Gap between tiles on DashboardGrid, DashboardLanes, and the edit overlay.
--wp-grid-drag-preview-scale 1.05 Lift scale of the drag-preview functional frame. Set to 1 to disable the lift.
--wp-grid-drag-preview-radius 0 Border radius of the drag-preview functional frame so the lift shadow follows the consumer's tile shape.
--wp-grid-placeholder-opacity 0.4 Opacity of the placeholder tile (the original item while a drag is in flight).
--wp-grid-placeholder-outline-style dashed Outline style of the drag placeholder (for example solid or dotted).
--wp-grid-resize-preview-outline-style solid Border style of the resize-preview overlay (for example dashed or dotted).
--wp-grid-placeholder-outline-color var(--wpds-color-stroke-interactive-brand) Outline color of the placeholder and of the resize-preview overlay.
--wp-grid-placeholder-radius 0 Border radius of the placeholder, used to match the consumer's tile shape so the outline traces the right silhouette.
--wp-grid-overlay-tile-bg var(--wpds-color-background-surface-neutral-weak) Background of the marker tiles painted by the default edit-mode overlay.

Contributing to this package

This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to npm and used by WordPress as well as other software projects.

To find out more about contributing to this package or Gutenberg as a whole, please read the project's main contributor guide.



Code is Poetry.

Keywords