Repository navigation
Block Supports: Add an experimental rotate block support - #84505
Draft
draganescu wants to merge 23 commits into
Draft
draganescu wants to merge 23 commits into
draganescu wants to merge 23 commits into
Conversation
Every block that doesn't opt out with `supports.rotate: false` can now be rotated, behind the new "Block rotation" experiment (`gutenberg-block-rotation`, which sets `window.__experimentalEnableBlockRotation`). The Dimensions panel gets an optional Rotation item with an angle picker. The rotation is a number of degrees in (-180, 180] stored in `style.rotate`. It follows the selected viewport style state: overrides live in `style['@tablet'].rotate` and `style['@mobile'].rotate`, where 0 is stored and undoes the default rotation on that viewport. In the default state, 0 is not stored. "Reset all" only clears the selected state's rotation. Blocks are rotated with the CSS `rotate` property rather than `transform`, which the editor writes to animate block moves. In the editor, the rules are added under a generated class, with viewport overrides in the responsive media queries. On the front end, `lib/block-supports/rotate.php` adds the same rules to the block supports stylesheet under a class generated from them, treating the stored value as untrusted. Saved markup doesn't change, and stored rotation is shown whether or not the experiment is on. `getRotateForState()` and `getUpdatedRotateStyle()` read and write the rotation of a style state, for tools such as a canvas rotate handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Classic, Custom HTML, Missing, More, Page Break, Pattern, Shortcode and synced patterns opt out with `supports.rotate: false`, like they opt out of custom CSS and visibility: they have no wrapper of their own to rotate, or lose it on save or on the front end. A block whose own `style` attribute is not an object doesn't support rotation, and style values that are not objects are read as no rotation. JS reads stored values like PHP: only decimal numeric strings, and halves rounded away from zero after removing floating point noise, so -12.345 is -12.35 on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 PR meta 🤖🏷️ LabelsThis pull request needs exactly one label indicating its type, and has 0.
Read more about Type labels in Gutenberg. If you cannot add labels yourself, a reviewer can do it for you. 📦 Bundle sizeSize Change: +798 B (+0.01%) Total Size: 9.07 MB 📦 View Changed
⚡ PerformanceShow the resultsClient side metrics exclude the server response time. front-end-block-theme
front-end-classic-theme
media-processing
media-upload
post-editor
site-editor
🏁 Flaky testsSome tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information. Backspace in
|
PHPCompatibility flags the '0x10' test value, which checks that hexadecimal strings are rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task
PHP 7 rejects numeric strings with trailing whitespace, while PHP 8 accepts them, so the PHP 7.4 test run failed on ' 15 '. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
draganescu
commented
Oct 10, 2026
# Conflicts: # packages/block-editor/CHANGELOG.md # packages/block-library/CHANGELOG.md
…port # Conflicts: # packages/block-editor/tsconfig.json
Trunk types useSettings as returning unknown values, so the viewport setting is cast to the record getRotateCSS expects, as other typed callers of useSettings do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rotate hook registered its own filter adding the style attribute. When the hook was imported early, such as by the grid's mobile stacking, the filter ran before the other block supports and moved the style attribute ahead of theirs, which changed serialized fixtures, and it added the attribute in tests that only load another block support. The style hook's filter now also adds it for rotation, so it stays in the same place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename the rotate block support and rotation utility tests to TypeScript so they pass the `gutenberg/use-typescript` lint rule for new files. The tests check the same things: block types are built with a typed helper, the block rotation experiment flag is read through a typed window, and the cases that pass a style attribute that is not an object use `@ts-expect-error`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…port # Conflicts: # packages/block-editor/src/hooks/test/style.jsdom.test.tsx
The rotate render_block filter and the editor's useBlockProps ran with the experiment off, so blocks storing style.rotate were rotated anyway. The front-end filter is now removed in lib/load.php while the experiment is off, which keeps rotate.php as it would be backported, and the editor support's hasSupport is isRotateEnabled, which gates both the control and the rotation. The style attribute is still registered for rotation on both sides, so a stored rotation is kept rather than dropped on save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove core's wp_render_rotate_support filter when it exists, as the custom CSS and block visibility supports do, so a WordPress version that ships rotation doesn't output the rules twice. The angle picker now rounds to two decimals, the same as stored rotations, through a shared roundAngle helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`getRotateValue`, `getRotateForState` and `getUpdatedRotateStyle` read and write `style.rotate` without any React or UI code, but lived in the rotate hook module, which imports the inspector controls. Grid code that only needs the data helpers had to import that module. They now live in `utils/rotation.ts` with the other angle helpers. The rotation utils test runs in jsdom, since the block style state helpers need it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block style state helpers only needed `hooks/utils` for `cleanEmptyObject`, and that module imports the components and the store. Through them, `utils/rotation.ts` couldn't load outside a browser environment. `cleanEmptyObject` is a plain object helper, so it now lives in `utils/object.ts`, and `hooks/utils` re-exports it for its existing importers. The rotation utils test runs in node again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What?
Adds a rotate block support, behind a new "Block rotation" experiment. Blocks get an optional Rotation control in their Dimensions panel unless they opt out with
supports.rotate: false. The rotation follows the selected viewport (Desktop, Tablet, Mobile). Split out of #84340, where rotation started as a grid-only value; the canvas rotate handle for blocks in a manual placement grid is #84512, at the top of the grid stack, which builds on this.Why?
Blocks can't be rotated today. This is the general support that the manual grid's canvas rotate handle builds on: the handle reads and writes the same data through the helpers added here.
How?
style.rotate. Viewport overrides live instyle['@tablet'].rotateandstyle['@mobile'].rotate, where 0 is stored and undoes the default rotation on that viewport. In the default state, 0 is not stored.AnglePickerControlin an optionalToolsPanelItemin the Dimensions panel. It edits the selected viewport, and its reset and the panel's "Reset all" only clear that viewport's rotation. It's hidden for pseudo states such as:hover.rotateproperty, nottransform, which the editor writes to animate block moves. Rules are added under a generated class, without!important, with the default rotation first and viewport overrides in the responsive media queries. This happens in the editor through a style override and on the front end through the block supports stylesheet (lib/block-supports/rotate.php). Saved markup doesn't change.0x10), rounded to two decimals with halves away from zero, and wrapped into (-180, 180].rotateis added to the safe CSS properties, because the style engine filters every declaration throughsafecss_filter_attr().styleattribute is not an object is left alone.supports.rotate: false, mirroring the blocks that opt out ofcustomCSSandvisibility: Classic, Custom HTML, Missing, More, Page Break, Pattern, Shortcode and synced patterns (core/block). Classic would lose the rotation on save, Custom HTML and synced patterns would rotate an editor wrapper that doesn't exist on the front end, and More and Page Break have no tag.states.php), following block visibility and custom CSS. The style engine route would need changes to the public style engine package; it would serialise an inline style into the saved markup of blocks that have other style supports; its editor output only covers blocks with style supports; andstates.phpemits viewport rules with!important, which would stop a canvas handle from previewing an angle with an inline style.styleattribute stays registered for rotation in JS and PHP so saving doesn't drop them, but blocks are shown unrotated. In the editor the support'shasSupportisisRotateEnabled(), which gates the control and the rotation together. On the front end,lib/load.phpremoves therender_blockfilter while the experiment is off, sorotate.phpstays as it would be backported. Without this, third-party blocks that store their ownstyle.rotatewould be rotated by Gutenberg with the experiment off.getRotateValue(),getRotateForState()andgetUpdatedRotateStyle()read and write a style state's rotation for other editor tools. They live inutils/rotation.tswith the angle helpers, not in the hook module, so tools that only need the data don't import the hook and its controls. To keep that module free of the hook utilities (which import the components and the store),cleanEmptyObjectmoved fromhooks/utils.tsxtoutils/object.ts;hooks/utils.tsxre-exports it, so its importers and the private API are unchanged.Known limitations:
transformtranslates along the rotated axes, so the block briefly travels at an angle. It settles in the right place.Still to do before this can leave draft:
lib/block-supports/rotate.php(aswp_render_rotate_support()andwp_register_rotate_support()) and therotateentry insafe_style_css: Editor: Add the rotate block support wordpress-develop#14707 (draft), withbackport-changelog/7.2/14707.md. Thesupports.rotate: falseopt-outs in Core's block.json files arrive with the package update.lib/block-supports/rotate.phpremove Core'srender_blockfilter before adding its own, asblock-visibility.phpandcustom-css.phpdo.supports.rotateinschemas/json/block.jsonand the block supports reference while it is experimental.Testing Instructions
style.rotateis removed from the block's attributes (Code editor).Testing Instructions for Keyboard
Tab to the Dimensions panel's options menu, turn on Rotation, then type an angle into the Rotation number field. Check that Tab moves through the panel and that "Reset all" works from the menu.
Screenshots or screencast
To add.
Use of AI Tools
Written with Claude Code (Claude Opus 5.5), split out of #84340. The design decisions (a general support stored in
style.rotate, a separate experiment) were made in conversation with the PR author.🤖 Generated with Claude Code