Skip to content

Block Supports: Add a Transform block support with a rotate feature - #86240

Draft
aaronrobertshaw wants to merge 15 commits into
trunkfrom
add/transform-block-support
Draft

aaronrobertshaw wants to merge 15 commits into
trunkfrom
add/transform-block-support

Conversation

@aaronrobertshaw

@aaronrobertshaw aaronrobertshaw commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Related:

Core backport:

What?

Adds a Transform block support with rotation as its first feature. Group, Heading, Image and Video opt in.

Why?

Site owners and themes cannot rotate a block. A separate Transform support lets scale, skew or translate follow without schema or attribute changes.

How?

  • Value: style.transform.rotate holds one CSS angle. Anything else outputs nothing.
  • Output: the individual rotate property, never transform.
  • Target: the wrapper, or the transform feature selector. Image and Video rotate the img and video, so captions stay level.
  • States: :hover and viewport rotations use the shared block states path. No opted-in block has a :hover state yet.
  • Global Styles: a Transform panel for blocks and variations, rendered in both site editors. settings.transform.rotate turns it off.
  • UI: a Transform panel using AnglePickerControl. It writes deg and shows other units in whole degrees.
  • Lightbox: the enlarged image stays unrotated.

Note

AnglePickerControl only accepts 0 to 360. A stored negative angle shows as entered, but becomes 0deg if the field is focused and left.

Testing Instructions

  1. Add an Image with a caption and lightbox. Set Styles > Transform > Rotation.
  2. Check only the image rotates, in the editor and front end. Open the lightbox; the enlarged image is level.
  3. On a Heading, choose Mobile, turn on Responsive styles and set a rotation. Desktop stays level.
  4. Give Heading a theme.json rotation of "-0.025turn". Global Styles shows -9°, and the canvas matches the front end.

Screenshots or screencast

Image with a caption, front end and lightbox

1-image-rotation.mp4

Rotation for Mobile only

2-heading-viewport.mp4

theme.json rotation in turns, in Global Styles

3-theme-json-turn.mp4

Use of AI Tools

Written with Claude Code, then reviewed and tested.

@aaronrobertshaw aaronrobertshaw added [Type] Enhancement A suggestion for improvement. [Feature] Block API API that allows to express the block paradigm. [Package] Blocks /packages/blocks [Package] Block library /packages/block-library [Package] Block editor /packages/block-editor [Feature] Design Tools Tools that impact the appearance of blocks both to expand the number of tools and improve the experi [Package] Style Engine /packages/style-engine labels Nov 3, 2026
@aaronrobertshaw aaronrobertshaw self-assigned this Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground Preview in Storybook

📦 Bundle size

Size Change: +1.28 kB (+0.01%)

Total Size: 8.58 MB

📦 View Changed
Filename Size Change
build/modules/lazy-editor/index.min.js 14.4 kB +13 B (+0.09%)
build/scripts/block-editor/index.min.js 543 kB +912 B (+0.17%)
build/scripts/block-library/index.min.js 415 kB +32 B (+0.01%)
build/scripts/blocks/index.min.js 44.9 kB +18 B (+0.04%)
build/scripts/core-data/index.min.js 76.7 kB +16 B (+0.02%)
build/scripts/edit-site/index.min.js 376 kB +30 B (+0.01%)
build/scripts/editor/index.min.js 559 kB +76 B (+0.01%)
build/scripts/style-engine/index.min.js 2.34 kB +95 B (+4.23%)
build/styles/block-library/image/style-rtl.css 3.02 kB +15 B (+0.5%)
build/styles/block-library/image/style-rtl.min.css 1.96 kB +14 B (+0.72%)
build/styles/block-library/image/style.css 3.03 kB +15 B (+0.5%)
build/styles/block-library/image/style.min.css 1.95 kB +11 B (+0.57%)
build/styles/block-library/style-rtl.css 22.9 kB +10 B (+0.04%)
build/styles/block-library/style-rtl.min.css 19.4 kB +8 B (+0.04%)
build/styles/block-library/style.css 23 kB +10 B (+0.04%)
build/styles/block-library/style.min.css 19.4 kB +7 B (+0.04%)

7aeccd0 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 98cbe65 trunk % Change
timeToFirstByte 56.7 ms +13.49% -2.38% 55.65 ms +4.31% -3.14% 1.89%
largestContentfulPaint 100 ms +6% -8% 94 ms +2.13% -6.38% 6.38%
lcpMinusTtfb 37.25 ms +19.46% -5.23% 35.25 ms +17.16% -5.25% 5.67%
wpBeforeTemplate 29.89 ms +16.53% -2.48% 29.02 ms +4.93% -1.86% 3%
wpTemplate 22.85 ms +3.5% -3.68% 21.91 ms +4.34% -2.97% 4.29%
wpTotal 52.76 ms +13.06% -2.27% 51.94 ms +4.47% -3.89% 1.58%
wpMemoryUsage 7.71 MB +0% -0% 7.67 MB +0% -0% 0.49%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 98cbe65 trunk % Change
timeToFirstByte 47.3 ms +5.39% -2.85% 46.3 ms +2.38% -2.7% 2.16%
largestContentfulPaint 102 ms +3.92% -1.96% 100 ms +0% -0% 2%
lcpMinusTtfb 54.4 ms +3.49% -1.29% 54.5 ms +1.19% -1.74% -0.18%
wpBeforeTemplate 26.87 ms +4.28% -0.82% 26.13 ms +2.91% -1.42% 2.83%
wpTemplate 17.03 ms +3.58% -5.46% 16.3 ms +5.95% -1.78% 4.48%
wpTotal 44.06 ms +5.88% -2.66% 43.16 ms +2.69% -2.87% 2.09%
wpMemoryUsage 6.40 MB +0% -0% 6.36 MB +0% -0% 0.61%
wpDbQueries 13 +0% -0% 13 +0% -0% 0%

media-processing

Metric 98cbe65 trunk % Change
mediaProcessingJpeg 400.47 ms +1.05% -0.51% 403.04 ms +1.61% -1.8% -0.64%
mediaProcessingAvif 6151.03 ms +0.22% -0.49% 6126.95 ms +0.62% -0.2% 0.39%
mediaProcessingJpegToAvif 4232.49 ms +0.1% -0.33% 4234.52 ms +0.18% -0.12% -0.05%

media-upload

Metric 98cbe65 trunk % Change
jpegUploadProcessing 1952.34 ms +0.91% -25.54% 1456.38 ms +2.35% -0.98% 34.05%
pngUploadProcessing 175.62 ms +5.35% -1.69% 176.73 ms +50.99% -5.1% -0.63%
largeJpegUploadProcessing 1707.63 ms +13.86% -14.85% 1454.79 ms +1.82% -0.4% 17.38%
multipleImageUploadProcessing 2026.51 ms +1.2% -0.81% 2039.42 ms +1.03% -0.02% -0.63%

post-editor

Metric 98cbe65 trunk % Change
serverResponse 417.15 ms +1.41% -8.13% 388.29 ms +5% -4.76% 7.43%
firstPaint 442.07 ms +7.54% -11.41% 371.79 ms +26.82% -20.44% 18.9%
domContentLoaded 829.9 ms +0.77% -1.58% 839.33 ms +1.94% -1.87% -1.12%
loaded 830.93 ms +0.77% -1.58% 840.18 ms +1.96% -1.85% -1.1%
firstContentfulPaint 478.74 ms +1.92% -2.63% 486.34 ms +4.72% -2.84% -1.56%
firstBlock 2050.92 ms +4% -0.59% 2050.83 ms +0.63% -0.7% 0%
type 17.93 ms +13.94% -11.27% 17.81 ms +1.97% -9.38% 0.67%
typeWithoutInspector 17.81 ms +1.46% -13.92% 17.01 ms +10.93% -9.17% 4.7%
typeWithTopToolbar 22.55 ms +4.88% -8.47% 23.62 ms +4.78% -11.69% -4.53%
typeContainer 8.04 ms +9.45% -0.87% 7.84 ms +19.26% -9.57% 2.55%
focus 114.64 ms +9.27% -3.93% 112.59 ms +4.57% -3.97% 1.82%
firstFocus 249.78 ms +0% -0% 246.48 ms +0% -0% 1.34%
selectAll 336.11 ms +5.94% -0.29% 322.15 ms +4.76% -1.97% 4.33%
undo 31.17 ms +2.86% -7.28% 28.75 ms +6.43% -2.82% 8.42%
listViewOpen 74.11 ms +5.46% -5.18% 69.43 ms +6.97% -5.73% 6.74%
inserterOpen 41.54 ms +6.57% -6.45% 40.26 ms +9.69% -6.98% 3.18%
inserterHover 11.99 ms +10.26% -9.26% 11.46 ms +5.67% -7.07% 4.62%
inserterSearch 8.72 ms +7.91% -3.33% 8.87 ms +3.95% -5.75% -1.69%
loadPatterns 668.4 ms +2.77% -1.72% 661.64 ms +6.84% -3.33% 1.02%
wpTotal 406.23 ms +1.65% -8.07% 378.56 ms +5.06% -4.91% 7.31%
wpMemoryUsage 14.76 MB +0% -0% 14.72 MB +0% -0% 0.27%
wpDbQueries 55 +0% -0% 55 +0% -0% 0%

site-editor

Metric 98cbe65 trunk % Change
serverResponse 453.82 ms +7.02% -4.27% 470.49 ms +2% -4.13% -3.54%
firstPaint 373.23 ms +13.03% -10.51% 338.95 ms +8.94% -13.91% 10.11%
domContentLoaded 895.89 ms +4.4% -3.45% 876.24 ms +2.06% -1.34% 2.24%
loaded 896.85 ms +4.39% -3.45% 877.3 ms +2.05% -1.36% 2.23%
firstContentfulPaint 509.79 ms +6.6% -1.76% 497.52 ms +4.94% -2.27% 2.47%
firstBlock 2623.62 ms +8.73% -3.82% 2577.78 ms +0.68% -0.41% 1.78%
type 19.01 ms +3.63% -8.42% 18.83 ms +10.04% -14.29% 0.96%
navigate 117.39 ms +8.86% -0.81% 114.29 ms +6.11% -3.32% 2.71%
loadPatterns 1351.59 ms +9.7% -6.21% 1367.25 ms +23.5% -11.32% -1.15%
loadPages 1001.79 ms +2.07% -2.45% 1043.12 ms +1.88% -5.85% -3.96%
wpTotal 443.99 ms +7.22% -4.29% 460.97 ms +2.03% -4.22% -3.68%
wpMemoryUsage 12.77 MB +0% -0% 12.73 MB +0% -0% 0.31%
wpDbQueries 45 +0% -0% 45 +0% -0% 0%

7aeccd0 Run

🏁 Flaky tests

Some 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.

Page List converts to Navigation Links with entity bindings in /test/e2e/specs/editor/blocks/navigation.spec.js, passed after 1 failed attempt.
Error: expect(locator).toContainText(expected) failed

Locator: getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ })
Expected substring: "test-page-1"
Received string:    "Link to:Cat/catPagePublished"
Timeout: 5000ms

Call log:
  - Expect "toContainText" getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ }) with timeout 5000ms
  - waiting for getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ })
    14 × locator resolved to <button type="button" aria-expanded="false" aria-haspopup="dialog" class="components-button link-preview-button is-next-40px-default-size is-secondary">…</button>
       - unexpected value "Link to:Cat/catPagePublished"

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/navigation.spec.js:1949:31
does not select blocks in the canvas preview when editing from the sidebar (Site Editor v2) in /test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js, passed after 1 failed attempt.
Error: expect(locator).toBeFocused() failed

Locator: getByRole('combobox', { name: 'Search or type URL' })
Expected: focused
Timeout: 5000ms
Error: element(s) not found

Call log:
  - Expect "toBeFocused" getByRole('combobox', { name: 'Search or type URL' }) with timeout 5000ms
  - waiting for getByRole('combobox', { name: 'Search or type URL' })

    at LinkControl.useLinkControlSearch (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js:416:47)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js:179:21
moves selected text within a paragraph in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 1
+ Received  + 1

  Array [
    Object {
      "attributes": Object {
-       "content": "second First third",
+       "content": "First second third",
      },
      "name": "core/paragraph",
    },
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:1209:41
should only mutate text data on input in /test/e2e/specs/editor/various/rich-text.spec.js, passed after 1 failed attempt.
Error: expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 4

- Array []
+ Array [
+   "Typing mutated more than character data: attributes",
+   "Typing should only mutate once.",
+ ]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/rich-text.spec.js:460:20
should select by dragging in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 1 failed attempt.
Error: expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 0

  Array [
    1,
-   2,
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:533:5

7aeccd0 Run

@aaronrobertshaw aaronrobertshaw mentioned this pull request Nov 3, 2026
1 of 3 tasks
Only the blocks from the original rotate PR opt in: Group, Heading, Image and Video. The :hover rotation tests now expect Button's root selector, and the e2e spec drops the Button hover test.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new bundle can call an undefined hook when paired with an older wp.blockEditor, crashing the block screen or Blocks list.

3 open issues
What changed in this PR

Adds Transform block support with rotation as its first feature, integrating editor controls, Global Styles, and front-end rendering. Group, Heading, Image, and Video opt in.

Changes:

  • Stores rotation in style.transform.rotate and outputs the individual CSS rotate property through wrapper or feature selectors.
  • Adds Transform panels and shared responsive/state styling, while keeping enlarged lightbox images unrotated.
  • Updates schemas, documentation, changelogs, and automated coverage.
File Description
test/​integration/​theme-schema.test.js Tests permitted rotation style locations.
test/​e2e/​specs/​editor/​various/​block-transform-rotate.spec.js Covers image rotation, captions, and lightbox behavior.
schemas/​json/​theme.json Defines transform settings and styles.
schemas/​json/​block.json Defines transform support and selectors.
phpunit/​style-engine/​style-engine-test.php Tests rotation declaration generation.
phpunit/​kses-rotate-test.php Tests allowed and rejected rotation values.
phpunit/​class-wp-theme-json-test.php Tests theme rotation rendering and sanitization.
phpunit/​block-supports/​transform-test.php Tests wrapper and feature-selector output.
phpunit/​block-supports/​states-test.php Tests responsive and hover rotation output.
packages/​style-engine/​src/​types.ts Adds rotation style typing.
packages/​style-engine/​src/​test/​index.js Tests JavaScript rotation compilation.
packages/​style-engine/​src/​styles/​transform/​index.ts Validates angles and generates rotation rules.
packages/​style-engine/​src/​styles/​index.ts Registers transform style definitions.
packages/​style-engine/​src/​class-wp-style-engine.php Adds PHP angle validation and declarations.
packages/​style-engine/​CHANGELOG.md Records transform style support.
packages/​global-styles-ui/​src/​screen-block.tsx Adds the block-level Transform panel.
packages/​global-styles-ui/​src/​screen-block-list.tsx Recognizes transform-enabled blocks.
packages/​global-styles-ui/​CHANGELOG.md Records Transform panel availability.
packages/​global-styles-engine/​src/​settings/​get-setting.ts Recognizes the rotation setting.
packages/​global-styles-engine/​CHANGELOG.md Records the new setting.
packages/​blocks/​src/​api/​constants.ts Maps rotation to transform support.
packages/​blocks/​CHANGELOG.md Records rotation property metadata.
packages/​block-library/​src/​video/​README.md Documents Video rotation support and targeting.
packages/​block-library/​src/​video/​block.json Enables rotation on the video element.
packages/​block-library/​src/​image/​style.scss Resets rotation on enlarged lightbox images.
packages/​block-library/​src/​image/​README.md Documents Image rotation support and targeting.
packages/​block-library/​src/​image/​block.json Enables rotation on the image element.
packages/​block-library/​src/​heading/​README.md Documents Heading rotation support.
packages/​block-library/​src/​heading/​block.json Enables Heading rotation.
packages/​block-library/​src/​group/​README.md Documents Group rotation support.
packages/​block-library/​src/​group/​block.json Enables Group rotation.
packages/​block-editor/​src/​hooks/​utils.tsx Includes rotation in resolved block settings.
packages/​block-editor/​src/​hooks/​types.ts Defines transform hook types.
packages/​block-editor/​src/​hooks/​transform.tsx Implements inspector integration and editor styling.
packages/​block-editor/​src/​hooks/​test/​transform.jsdom.test.ts Tests transform style routing.
packages/​block-editor/​src/​hooks/​test/​style.jsdom.test.tsx Tests hover and responsive rotation rules.
packages/​block-editor/​src/​hooks/​supports.ts Recognizes transform as a style support.
packages/​block-editor/​src/​hooks/​style.tsx Integrates controls and delegates transform serialization.
packages/​block-editor/​src/​hooks/​index.ts Registers the transform editor hook.
packages/​block-editor/​src/​components/​inspector-controls/​groups.ts Adds the transform controls group.
packages/​block-editor/​src/​components/​inspector-controls-tabs/​use-inspector-controls-tabs.ts Includes transform controls in tab detection.
packages/​block-editor/​src/​components/​inspector-controls-tabs/​styles-tab.tsx Adds the Transform panel slot.
packages/​block-editor/​src/​components/​global-styles/​types.ts Defines transform settings and panel types.
packages/​block-editor/​src/​components/​global-styles/​transform-panel.tsx Adds rotation controls and angle conversion.
packages/​block-editor/​src/​components/​global-styles/​test/​transform-panel.jsdom.test.tsx Tests conversion, editing, and reset behavior.
packages/​block-editor/​src/​components/​global-styles/​index.ts Exports the Transform panel and availability hook.
packages/​block-editor/​src/​components/​global-styles/​hooks.ts Disables rotation controls where unsupported.
packages/​block-editor/​src/​components/​block-inspector/​index.tsx Adds default and state-specific Transform slots.
packages/​block-editor/​CHANGELOG.md Records the panel and inspector group.
lib/​theme.json Enables rotation controls by default.
lib/​load.php Loads transform block support.
lib/​compat/​wordpress-7.3/​kses.php Allows and validates rotation declarations.
lib/​class-wp-theme-json-gutenberg.php Handles transform settings, sanitization, and CSS output.
lib/​block-supports/​transform.php Registers attributes and renders block rotations.
docs/​reference-guides/​theme-json-reference/​theme-json-living.md Documents transform settings and styles.
docs/​reference-guides/​theme-json-reference/​styles-versions.md Records rotation version availability.
docs/​reference-guides/​core-blocks/​README.md Lists rotation support on opted-in blocks.
docs/​reference-guides/​block-api/​block-supports.md Documents the transform support contract.
docs/​how-to-guides/​themes/​global-settings-and-styles.md Adds rotation to the settings example.

Review effort: Balanced

Comment thread schemas/json/theme.json Resolved
theme.json and Global Styles rotations rendered on the front end but not
in the post or site editor canvas, because STYLE_KEYS left out transform.
Also lists transform in the Global Styles change summary.
A fractional angle, such as 1rad shown as 57.3, failed the angle picker's
step and was rewritten as soon as the field lost focus.
PHP's $ matched before a final newline, so "45deg\n" passed in PHP but
not in JS.
Elements already forbid transform, but their :hover and other states
referenced the complete style properties, which now include it. The PHP
side strips it, so the schema now rejects it too.
The select and text input states still referenced the complete style properties, which include transform. Guard them the same way as the link and button states.
@tellthemachines

Copy link
Copy Markdown
Contributor

Thanks for putting this together! I just did a quick round of testing and it's looking really good.

I'm not too sure about rotating only image/video without the captions, but I guess folks who want to rotate the whole block can always wrap it in a Group and rotate that instead:

Screenshot 2026-11-04 at 5 20 15 pm

This is also a great foundation to add a translate transform to, which can be used to place items anywhere on a grid without snapping them to the grid cells. So it does feel like a more solid solution than just the rotate support.

@aaronrobertshaw

Copy link
Copy Markdown
Contributor Author

Thanks for the early review @tellthemachines 🚀

I'm not too sure about rotating only image/video without the captions

Agreed. In part this helped test the block support with the selectors API and skipping serialization. Happy to allow it to go straight onto the wrapper.

This is also a great foundation to add a translate transform to

I have a draft up for this too actually. We should be able to polish and merge these pretty soon I'd say. That is, assuming we are all good with leaving Z axis translation to a follow up.

@tellthemachines

Copy link
Copy Markdown
Contributor

That is, assuming we are all good with leaving Z axis translation to a follow up.

Yeah I'm not sure we have any use for translateZ so definitely not worth adding now.

@tellthemachines tellthemachines mentioned this pull request Nov 4, 2026
34 tasks

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Block API API that allows to express the block paradigm. [Feature] Design Tools Tools that impact the appearance of blocks both to expand the number of tools and improve the experi [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Package] Blocks /packages/blocks [Package] Style Engine /packages/style-engine [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants