Skip to content

Grid: Canvas exploration - #84340

Closed
draganescu wants to merge 12 commits into
trunkfrom
add/grid-drag-rotate-guides
Closed

draganescu wants to merge 12 commits into
trunkfrom
add/grid-drag-rotate-guides

Conversation

@draganescu

@draganescu draganescu commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Explores the ideas in #81796

What?

Makes moving and arranging blocks in a manual-placement grid (Group block, Grid layout, "Grid item position: Manual") feel direct. All of it sits behind the existing gutenberg-grid-interactivity experiment:

  • Drag with cell snapping. Dragging a block with the block toolbar's drag handle shows exactly which cells it will land in. The pointer marks the centre of the block, measured without its rotation, so it lands in the cells closest around the pointer. Cells already used by another block get a hatched tint, because overlap is allowed.
  • Alignment guides. While dragging, lines show when the landing spot lines up with the grid's edges or centre, or with the nearest block on each edge and centre.
  • Rotation. A rotate handle on the selected block snaps to 15° steps (Shift rotates freely, Escape cancels). The block settings get a Rotation control, which is also the keyboard route. Stored as style.layout.rotate, with per-viewport overrides.
  • Mobile stacking. Manual grids stack their blocks on mobile (full width, block order, row spans kept, unrotated) unless "Stack on mobile" is turned off on the grid. Editing a block in the Mobile style state turns that stack into the grid's own mobile layout, so nothing jumps, then applies the edit.
  • Rows grow when resizing. Dragging a block's bottom resize handle past the last row adds rows.
  • Grid row handles. A selected manual grid has handles on its top and bottom edges that add or remove rows. Rows added or removed at the top move every block with them, and rows with blocks in them can't be removed. With the rows the grid has, a dragged block stays in the grid; dropped outside it, the block moves to wherever it lands. Drags work in both directions: over the grid, the grid's landing cells show; elsewhere, the block flow's insertion indicator does, wherever the drag started.
  • Draw to insert. Pressing on an empty cell and dragging selects a rectangle of cells. On release, the inserter opens, and the block inserted covers the selected cells.
  • Same size cells. A manual grid takes its height from its width (aspect-ratio: columns / rows), so all its cells are the same size, close to square, and content never makes them bigger. Images cover their cells. On mobile, the stack goes back to sizing blocks by their content.

Why?

Manual grid placement exists, but the only way to place a block precisely today is the arrow movers or typing column and row numbers. The built-in drag-and-drop gives no sense of where a block will land or what it lines up with. The interaction model is inspired by freeform canvas editors, kept to the grid's existing cell-based data so existing content and front-end CSS keep working.

How?

  • One drop zone for the whole grid (components/grid/grid-visualizer.jsx) replaces the per-cell drop zones. On dragover it maps the pointer to a cell using the grid's live track sizes (get-grid-drop-target.js), then works out overlaps and guides (get-alignment-guides.js). On drop it writes columnStart/rowStart and reorders the block so block order follows visual order, as before. Drags still use native HTML5 drag-and-drop.
  • No ghost. In manual grids a dragged block stays in place, dimmed, instead of following the pointer (use-selected-block-event-handlers.js). Drags use the block toolbar's existing drag handle; there is no extra grip on the block.
  • Rotation uses the CSS rotate property, not transform, because the editor's block-move animation writes transform on block elements. rotate is added to the safe CSS list in lib/compat/wordpress-7.2/kses.php. The editor (hooks/layout-child.jsx) and PHP (lib/block-supports/layout.php) both output it, and PHP wraps untrusted values into (-180, 180]. Overlays for rotated blocks are rotated to match (use-rotated-overlay-style.js).
  • Mobile stacking is one rule on the grid, inside the @mobile media query, with a doubled selector so it beats each child's own placement rule (layouts/grid.jsx, hooks/layout.jsx, and the grid branch in layout.php). Each child publishes its row span as --wp--grid-item--row-span so the stack keeps it.
  • Editing on mobile goes through useUpdateGridChildLayout, which, for a stacked grid in the Mobile style state, first writes mobile overrides that match the stack (mobile-stacking.js), then the edit, in one block update.
  • Leaving manual mode (switching to Auto, or moving a block out of the grid) clears rotation, as it already clears placement (use-grid-layout-sync.js).
  • All of this lives in block-editor, so it applies to both the post-based site editor and the extensible site editor.

Still to do before this can leave draft:

  • End-to-end tests.
  • A Core backport PR and its backport-changelog entry for the layout.php and kses.php changes.
  • Decide how resizing a rotated block should work. The handles follow the rotation, but dragging them still resizes along the screen's axes.

Testing Instructions

  1. Enable Gutenberg > Experiments > "Grid interactivity".
  2. Insert a Group block, choose the Grid layout, and in the block settings set "Grid item position" to "Manual".
  3. Add a few blocks to the grid, including an image.
  4. Select a block and drag it by the drag handle in the block toolbar. Check that the landing cells, overlap tint and guides follow the pointer, and that the block lands where shown.
  5. Drag an image by its body. Check that it stays in place, dimmed, while the indicator moves.
  6. Drag the round handle below a selected block to rotate it. Check the 15° snapping, Shift for free rotation, and Escape to cancel. One undo should revert the rotation.
  7. Drag a block's bottom resize handle below the last row. Check that rows are added, and that all cells stay the same size.
  8. Select the grid and drag the handle on its bottom edge down, then the one on its top edge up. Check that rows are added at that edge, that blocks move with rows added at the top, and that rows with blocks in them can't be removed.
  9. Press on an empty cell and drag across a few cells. On release, choose a block in the inserter, and check that it covers the selected cells.
  10. Drag a block from the grid and drop it below the grid. Check that it leaves the grid. Then drag a block from outside the grid, or a grid block whose toolbar sits outside the grid, over the page and then into the grid. Check that the block flow's insertion indicator shows outside the grid, that the grid's landing cells show inside it, and that the block lands where shown.
  11. Add an image and some long text to the grid. Check that the image covers its cells, and that neither changes the size of the cells.
  12. Switch the block sidebar's style state to Mobile. Check that the grid stacks, then drag or rotate a block. The other blocks should stay where the stack showed them.
  13. View the front end on a narrow screen. Check that the blocks stack, and that the rotation shows on wide screens.

Testing Instructions for Keyboard

  1. Select a block in a manual grid and use the arrow movers in the block toolbar to move it between cells.
  2. In the block settings, open the Rotation control from the Dimensions panel menu, and type an angle or use the angle picker.
  3. In the grid's settings, toggle "Stack on mobile".

The drag handle, rotate handle, row handles and drawing cells are pointer-only, like the existing resize handles. The movers, the grid's Rows control, the cell appenders and the sidebar controls cover the same actions, except that a block inserted from the keyboard covers one cell.

Screenshots or screencast

To add.

Use of AI Tools

Written with Claude Code (Claude Opus 5.5). The design decisions were made by the PR author in conversation with it; the code, unit and PHP tests, and this description were generated by it.

🤖 Generated with Claude Code

draganescu and others added 7 commits October 8, 2026 12:07
Pure functions for the manual grid drag, guide and rotate interactions,
with unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add a rotate child layout value, output with the CSS rotate property in
  the editor and on the front end, with viewport overrides.
- Allow the rotate property through safecss_filter_attr.
- Stack the children of manual placement grids on mobile unless the grid
  sets stackOnMobile to false.
- Clear rotation when a block leaves the grid or the grid switches to
  auto placement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In manual placement grids:
- One drop zone covers the grid and works out the landing cells from the
  pointer, keeping the grabbed cell under it and clamping to the grid.
- The landing cells, overlaps with other blocks, and alignment guides to
  the container and nearby blocks are drawn while dragging.
- Dragged blocks stay in place, dimmed, instead of following the pointer.
- A grip and a rotate handle sit on the selected block. Rotation snaps to
  15 degrees, Shift rotates freely, and Escape cancels.
- A Rotation control in the block settings, and a Stack on mobile toggle
  in the grid settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Children of manual placement grids publish their row span as the
--wp--grid-item--row-span custom property, and the grid's mobile stacking
rule spans that many rows instead of one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- On mobile, the grip, rotate and resize handles work on a stacked grid.
  The first edit gives the grid its own mobile layout matching the stack
  (full width, block order, row spans kept, unrotated), then applies the
  edit, as one undo step. Sidebar edits do the same.
- In manual grids, dragging the bottom resize handle past the last row
  adds rows, and resizes are measured from the grid's content box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the [Package] Block editor /packages/block-editor label Oct 8, 2026
draganescu and others added 2 commits October 8, 2026 16:20
Rows were sized by their own content, so cells in the same grid had
different heights. Rows now use `1fr`, which makes each one as tall as
the tallest, and rows outside the row count match them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removes the grip on selected grid items, so drags start from the block
toolbar's drag handle. Since a drag no longer starts on the block, the
pointer now marks the centre of the dragged block, measured without its
rotation, instead of the cell it was grabbed by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@draganescu draganescu added [Feature] Layout Layout block support, its UI controls, and style output. [Type] Enhancement A suggestion for improvement. labels Oct 8, 2026
draganescu and others added 3 commits October 8, 2026 17:03
Manual grids take their height from their width with
`aspect-ratio: columns / rows`, so their cells are all the same size,
close to square, and content no longer makes them bigger. Images cover
their cells, below any caption; in the editor, images in a manual grid
leave out their default inline `height: auto` so the layout can size
them. On mobile, a stacked grid goes back to sizing blocks by their
content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pressing on an empty cell of a manual grid and dragging selects a
rectangle of cells. On release, the inserter opens, and the block
inserted covers the selected cells.

A selected manual grid has handles on its top and bottom edges that add
or remove rows in the selected style state. Rows added or removed at
the top move every block with them, and rows with blocks in them can't
be removed. Blocks dragged out of the grid drop wherever they land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once a drag has been over the block flow, the block tools hide every
popover in their slots, including the grid's drop layer. A drag that
started outside a manual grid, such as from a block toolbar shown
above it, then fell back to the block flow's drop zones inside the
grid too.

While blocks are dragged, a manual grid's visualizer now renders in the
default popover slot, so blocks move between the grid and the block
flow in both directions: over the grid, its landing cells show, and
elsewhere the block flow's insertion indicator does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tellthemachines

Copy link
Copy Markdown
Contributor

Thanks for getting this started!

A few thoughts from a quick round of testing:

  • I love "rotate" and can't think of any reason for it to be tied to a grid layout or its manual mode; we should make it a block support in its own right. Maybe for regular blocks we don't show the handy rotation UI (or maybe we do!) but rotate: 45deg; can be added to any block and pretty much just work. Perhaps it could be another option in the Position block support?
  • The drag and drop UI is working better than it was but it's still underwhelming to have to drag from the toolbar; ideally for this we'd drag from the block itself.
  • "stack on mobile" is kinda disappointing: if we want this to be easy and tempting to use by regular humans who are not software engineers or CSS nerds, we need to make it look amazing on phones. The grid can be made fully responsive by setting a min column width in addition to a max columns; items occupying more than one grid cell are kinda responsive already but we can improve their behaviour I'm sure. A few UX/UI decisions needed around whether to set max cols/min col width by default for manual grids or to somehow show folks what they need to do to make their grids responsive. (There are two options there: either setting both max cols and min col width, or editing col numbers manually for each viewport. i guess the latter would be a more advanced way of doing it)

In terms of how to proceed: this is a massive, unreviewable changeset to a feature that is very bug-prone and needs careful testing to get right. We need to split this up into discrete pieces; pretty much each of the bullet points in the "what" section of the PR description could be its own PR. That way we can review and test each bit properly and be confident it's working well. I'm happy to help with reviews and putting together some of the PRs where needed (I had intended to devote some time to this project post-7.2 in any case)

@draganescu

Copy link
Copy Markdown
Contributor Author

Cool review @tellthemachines and lovely plan. I'll setup as you described as it all makes perfect sense.

@draganescu

draganescu commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

This PR is now split into one draft PR per feature.

Grid stack

Each PR builds on the one before it, and they are linked as a GitHub stack. From #84512 up, the branches have the full feature set of this PR.

  1. Grid: Give manual placement grids same-size cells #84499 Same-size cells for manual placement grids
  2. Grid: Snap drags to cells in manual placement grids #84500 Drag blocks in manual grids with cell snapping. This one also changes a public hook, useDropZone in @wordpress/compose (see below).
  3. Grid: Show alignment guides while dragging in manual grids #84501 Alignment guides while dragging
  4. Grid: Add and remove rows of manual placement grids #84502 Row handles on the grid, and resizing a block past the last row
  5. Grid: Draw blocks across cells in manual placement grids #84503 Draw on empty cells to insert a block
  6. Grid: Stack manual grids on mobile, and edit them from the stack #84509 Stack manual grids on mobile, and edit them from the stack
  7. Grid: Rotate manual grid items from the canvas #84512 Rotate handle for blocks in manual grids. This one also merges the two PRs below, so it is the complete feature set.
  8. Grid: Show block toolbars over manual grids and add a drag handle #84894 Block toolbars show over the grid rather than over each block, and a drag handle on the canvas moves blocks such as Paragraph or Heading, which can't be dragged by their body. Added after the split.
  9. Grid: Show the canvas handles in a small toolbar, and add a Canvas block #84947 The canvas handles (drag, rotate) in one small toolbar under the block, with a button that deletes the block, and a Canvas block in the inserter: a full width 12 by 12 manual grid. Added after the split, to try how manipulating blocks feels with the handles together.

Separate PRs

What changed compared with this PR

  • Rotation is a general block support (style.rotate), not a grid value (style.layout.rotate), so leaving manual mode no longer clears it. Blocks without a wrapper of their own (Classic, Custom HTML, synced patterns, More, Page break, Shortcode) opt out.
  • All grid CSS changes only apply with the "Grid interactivity" experiment on. Content that already uses manual grids, such as the navigation overlay pattern, renders exactly as on trunk otherwise.
  • Grids with a minimum column width keep equal rows but don't get the aspect ratio. Their columns wrap, and a fixed height would make content overflow. Blocks only fill their cells in grids sized by their width, so they never collapse.
  • Public API change in Grid: Snap drags to cells in manual placement grids #84500: useDropZone now also starts a drag on the first dragover over the zone when the document got no dragenter, so such a zone now gets onDragStart and onDragEnd. Chrome can skip that dragenter in the editor canvas iframe after a drag that left it over the grid's drop layer. Details are in Grid: Snap drags to cells in manual placement grids #84500's description.
  • Bugs found in review and testing are fixed. Some of them also exist in this PR:
    • Dropping a grid onto itself froze the editor.
    • Inline images in an image caption collapsed the main image.
    • The rotate handle drifted after a block changed size.
    • A drag could leave the editor stuck in its dragging state.
    • Sidebar edits on a grid stacked on mobile lost the block's position.
    • A block drawn in the Tablet or Mobile state had no desktop position.
    • After a drag, the inserters of empty cells covered the resize, row, rotate and drag handles, so clicks on them opened the inserter.
    • From the second drag of the same block on, the resize handles and the grid cells disappeared until another block was dragged (the useDropZone fix above).
    • In Safari, the cell that was under the drag handle stayed highlighted after a drop, and dragging an image block showed the image as a drag preview.

Status

  • All branches are up to date with trunk as of October 16 (5c1a5e0). They were updated by merging trunk into each branch, from the bottom of the stack up, without rewriting history. The only conflicts were in packages/block-editor/tsconfig.json, whose exclude list trunk removed: the stack's tests are now type checked like the rest. Add a rotation block support #84505 needed one fix for trunk's newly typed useSettings. Apart from that, each PR's diff is unchanged.
  • On the top of the stack (Grid: Show block toolbars over manual grids and add a drag handle #84894, which contains every PR): the type check, unit tests for block-editor, compose, block-library and base-styles, ESLint, Stylelint, phpcs, and the layout and rotate block supports PHPUnit tests pass. Each PR was reviewed and fixed by separate agents.
  • The stack has been tested in Chrome, Firefox and Safari, and the bugs found are fixed (see above).
  • CI is running again after the update. "Check for a Core backport changelog entry" fails on the PRs that change lib/. A Core backport is listed as still to do where it applies.

@draganescu

This comment was marked as off-topic.

@draganescu

This comment was marked as off-topic.

@draganescu draganescu changed the title Grid: Drag with cell snapping and guides, rotation, and mobile stacking for manual grids Grid: Canvas exploration Oct 16, 2026
@draganescu

Copy link
Copy Markdown
Contributor Author

I will keep the stack up to date and close this exploration.

@draganescu draganescu closed this Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Layout Layout block support, its UI controls, and style output. [Package] Block editor /packages/block-editor [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants