Skip to content

[WIP] Block Supports: Remove global border style fallbacks - #78818

Draft
aaronrobertshaw wants to merge 5 commits into
trunkfrom
try/border-style-editor-defaults
Draft

aaronrobertshaw wants to merge 5 commits into
trunkfrom
try/border-style-editor-defaults

Conversation

@aaronrobertshaw

Copy link
Copy Markdown
Contributor

Warning

This approach is open for evaluation. It removes the global CSS fallback from every page render, but it is not yet clear that the JS/PHP replacement is objectively better than simply keeping the recently tightened CSS rule.

What?

Follow up to #77476.

Replaces the legacy :where([style*="border-…"]) CSS visibility fallback for the border block support with a cascade-aware, edit-time intelligent default plus a render-time fallback for legacy content.

Why?

The legacy CSS rule in packages/block-library/src/common.scss forced border-style: solid on any element whose inline style attribute contained the substring border-color, border-width, etc. That substring match produced false positives:

• Any element with a CSS custom property such as --border-color: red was matched.
• Any element with a background-image: url(...border-color...) URL was matched.
• Any future inline-style declaration containing those substrings would be matched.

#77476 mitigated the symptom by tightening the selectors. This PR removes the root cause: the CSS fallback itself.

The CSS fallback has carried a // TODO for a long time noting it should be replaced with "intelligent defaults" once edit-time context made that possible. That context now exists (useBlockSettings, the resolved global-styles tree exposed via globalStylesDataKey, and block-style variation resolution helpers).

How?

Three coordinated mechanisms replace the one CSS rule. All are cascade-aware.

  1. Edit-time intelligent default (new content)

packages/block-editor/src/hooks/border.js the block-instance BorderPanel.onChange now calls a new pure helper applyBorderStyleDefaults. When the user sets a border color or width and:

• there is no explicit border.style on the value being persisted, AND
• there is no inherited global border style for the affected scope (shorthand vs. per-side),

then style: 'solid' is persisted into the block's attributes.style.border. Saved HTML becomes self-describing: no runtime fallback is needed to render the border. The shared StylesBorderPanel used by the Global Styles UI is intentionally untouched, its merged inheritedValue already handles inheritance, so no change is needed there.

Inheritance is resolved across two cascade levels:

• styles.blocks[ name ].border (block-type global styles)
• styles.blocks[ name ].variations[ active ].border (active block style variation, derived from the block's className via the shared getVariationNameFromClass helper)

Root styles.border.style is intentionally excluded, theme.json compiles it to a body selector and border-style is not a CSS-inherited property, so it does not cascade to inner blocks. Non-rendering values (none, hidden) are also excluded so that a theme setting border.style: 'none' does not silently suppress the fallback when a user adds a color.

  1. Editor render-time fallback (legacy content in the editor)

The same useBlockProps hook gains a render-time fallback via getBorderStyleFallbacks so that blocks with saved attributes carrying color/width but no style still render visibly in the editor wrapper. The fallback flows through BlockListBlock wrapper props; it does not mutate saved HTML.

  1. Front-end render-time fallback (legacy content on the front end)

lib/block-supports/border.php, new gutenberg_render_block_border_fallback hooked into the render_block filter, using WP_HTML_Tag_Processor to inject per-side border-{side}-style: solid into the wrapper's style attribute. Hooked into render_block rather than apply_block_supports because WP_Block_Supports::apply_block_supports() is only invoked via get_block_wrapper_attributes() from inside a dynamic block's render_callback, static blocks (Group, Paragraph, Heading, …) serialize their wrapper attributes at save time and never reach that path. The render_block filter covers both static and dynamic blocks.

The PHP path mirrors the JS algorithm exactly. Both files cross-reference each other in doc comments. The PHP helper has a quick-exit when the block has no border-related attribute and shares the same none/hidden non-rendering style handling.

Removed

packages/block-library/src/common.scss — the entire :where(.has-border-color, [style*="border-…"]) fallback block (lines 114-162) is removed.

Testing Instructions

  1. Activate Gutenberg trunk + this branch.
  2. Insert a Group block, open the Border panel, pick a color and width without explicitly selecting a style. Confirm the border is visible immediately in the editor and after publish.
  3. Inspect the saved post content (Code Editor view). Confirm the block markup carries "style":{"border {"color":"…","width":"…","style":"solid"}} — i.e. style: 'solid' is persisted into attributes.
  4. Save a block whose attributes have only style.border.color (no style.border.style) — easiest via the Code Editor. Publish and view on the front end. Confirm the border still renders as solid (backward compatibility).
  5. With a theme.json that sets styles.blocks.core/group.border.style = 'dashed', edit a Group block, add a border.color from the UI. Confirm the editor shows the inherited dashed border (not forced solid), and that style: 'solid' is not persisted into the block's attributes (cascade-aware).
  6. Regression coverage for Common CSS: avoid false-positive border-style on custom properties #77476 false-positives:
    ◦ Insert an HTML block with <div style="background-image: url('/wp-content/uploads/border-color-thing.png');">…</div>. Publish and inspect the rendered output: the element must have border-style: none (computed), not solid.
    ◦ Same with <div style="--border-color: red;">…</div>.
  7. Run the automated suites locally:
    ◦ npm run test:unit -- packages/block-editor/src/hooks/test/border.js (32 tests).
    ◦ npm run test:unit:php -- --filter WP_Block_Supports_Border_Test (29 tests).
    ◦ npm run test:e2e -- test/e2e/specs/editor/blocks/border-block-support.spec.js (3 tests).

Screenshots or screencast

TBA

@aaronrobertshaw aaronrobertshaw self-assigned this May 29, 2026
@aaronrobertshaw aaronrobertshaw added [Type] Enhancement A suggestion for improvement. Global Styles Anything related to the broader Global Styles efforts, including Styles Engine and theme.json [Feature] Design Tools Tools that impact the appearance of blocks both to expand the number of tools and improve the experi labels May 29, 2026
@github-actions github-actions Bot added [Package] Block library /packages/block-library [Package] Block editor /packages/block-editor labels May 29, 2026
@aaronrobertshaw
aaronrobertshaw force-pushed the try/border-style-editor-defaults branch from c86ec85 to 0d15af0 Compare October 16, 2026 12:05
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

📦 Bundle size

Size Change: -1.87 kB (-0.02%)

Total Size: 9.04 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 604 kB +437 B (+0.07%)
build/styles/block-library/common-rtl.css 2.49 kB -402 B (-13.88%) 👏
build/styles/block-library/common-rtl.min.css 1.31 kB -172 B (-11.58%) 👏
build/styles/block-library/common.css 2.52 kB -407 B (-13.9%) 👏
build/styles/block-library/common.min.css 1.31 kB -173 B (-11.65%) 👏
build/styles/block-library/style-rtl.css 22.4 kB -424 B (-1.86%)
build/styles/block-library/style-rtl.min.css 19.2 kB -154 B (-0.8%)
build/styles/block-library/style.css 22.5 kB -421 B (-1.84%)
build/styles/block-library/style.min.css 19.2 kB -155 B (-0.8%)

43b506e Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 97b8af9 trunk % Change
timeToFirstByte 58.1 ms +11.1% -7.66% 55 ms +9.27% -2.64% 5.64%
largestContentfulPaint 96 ms +10.42% -6.25% 92 ms +4.35% -4.35% 4.35%
lcpMinusTtfb 37.4 ms +15.51% -12.43% 34.95 ms +6.29% -9.16% 7.01%
wpBeforeTemplate 29.55 ms +19.26% -2.88% 28.63 ms +14.11% -1.43% 3.21%
wpTemplate 22.77 ms +2.72% -6.54% 22 ms +4% -4.18% 3.5%
wpTotal 54.33 ms +10.38% -8.04% 51.39 ms +6.91% -3.09% 5.72%
wpMemoryUsage 7.64 MB +0% -0% 7.61 MB +0% -0% 0.37%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 97b8af9 trunk % Change
timeToFirstByte 46.7 ms +3.64% -3% 45.55 ms +5.16% -2.09% 2.52%
largestContentfulPaint 102 ms +1.96% -1.96% 100 ms +0% -2% 2%
lcpMinusTtfb 55 ms +1.55% -1.55% 53.6 ms +3.36% -4.66% 2.61%
wpBeforeTemplate 26.49 ms +5.81% -2.49% 26.31 ms +2.13% -2.81% 0.68%
wpTemplate 16.66 ms +4.98% -3.48% 16.38 ms +2.08% -4.09% 1.71%
wpTotal 43.46 ms +3.73% -2.6% 42.36 ms +5.48% -1.49% 2.6%
wpMemoryUsage 6.32 MB +0% -0% 6.30 MB +0% -0% 0.4%
wpDbQueries 13 +0% -0% 13 +0% -0% 0%

media-processing

Metric 97b8af9 trunk % Change
mediaProcessingJpeg 405.39 ms +0.52% -1.06% 403.46 ms +0.7% -0.78% 0.48%
mediaProcessingAvif 6183.67 ms +0.79% -0.3% 6126.47 ms +0.24% -0.52% 0.93%
mediaProcessingJpegToAvif 4279.76 ms +0.13% -0.16% 4216.6 ms +0.26% -0.39% 1.5%

media-upload

Metric 97b8af9 trunk % Change
jpegUploadProcessing 1943.57 ms +2.11% -0.39% 1925.3 ms +0.59% -0.54% 0.95%
pngUploadProcessing 185.99 ms +8.48% -3.9% 178.09 ms +19.31% -9.48% 4.44%
largeJpegUploadProcessing 1917.6 ms +0.84% -0.24% 1920.24 ms +0.37% -0.19% -0.14%
multipleImageUploadProcessing 2023.5 ms +1.46% -0.35% 2051.51 ms +0.84% -1.41% -1.37%

post-editor

Metric 97b8af9 trunk % Change
serverResponse 522.2 ms +3.53% -8.1% 485.34 ms +2.01% -4.58% 7.59%
firstPaint 496.66 ms +6.44% -46.67% 428.9 ms +23.77% -39.48% 15.8%
domContentLoaded 1237.34 ms +1.73% -3.21% 1211.95 ms +2.93% -1.17% 2.09%
loaded 1238.75 ms +1.7% -3.2% 1213.22 ms +2.93% -1.16% 2.1%
firstContentfulPaint 527.14 ms +2.27% -3.65% 531.49 ms +0.53% -3.05% -0.82%
firstBlock 3650.53 ms +1.37% -0.82% 3667.14 ms +3.32% -1.88% -0.45%
type 17.96 ms +2.62% -0.89% 19.55 ms +2.66% -4.35% -8.13%
typeWithoutInspector 18.27 ms +3.12% -4.98% 18.34 ms +9.76% -3.65% -0.38%
typeWithTopToolbar 23.12 ms +2.51% -3.16% 24.96 ms +3.73% -1.64% -7.37%
typeContainer 8.93 ms +5.04% -7.84% 8.9 ms +5.17% -10.56% 0.34%
focus 124.93 ms +8.37% -5.07% 120.58 ms +3.11% -5.17% 3.61%
firstFocus 250.67 ms +0% -0% 235.92 ms +0% -0% 6.25%
selectAll 584.57 ms +1.26% -2.99% 588.77 ms +1.58% -6.7% -0.71%
listViewOpen 85.43 ms +16.22% -6.8% 90.46 ms +6.79% -16.79% -5.56%
inserterOpen 41.25 ms +15.01% -4.73% 41.95 ms +14.95% -2.48% -1.67%
inserterHover 13.28 ms +7.38% -9.94% 13.27 ms +6.41% -11.23% 0.08%
inserterSearch 8.92 ms +2.69% -3.25% 8.49 ms +1.65% -4.12% 5.06%
loadPatterns 676.82 ms +5.33% -3.26% 711 ms +9.44% -3.89% -4.81%
wpTotal 512.36 ms +3.33% -8.24% 475.83 ms +1.75% -4.6% 7.68%
wpMemoryUsage 14.67 MB +0% -0% 14.63 MB +0% -0% 0.23%
wpDbQueries 54.5 +0.92% -0.92% 54.5 +0.92% -0.92% 0%

site-editor

Metric 97b8af9 trunk % Change
serverResponse 318.89 ms +4.6% -6.65% 320.97 ms +2.22% -10.04% -0.65%
firstPaint 264.2 ms +61.28% -24.79% 218.92 ms +3.66% -5.56% 20.68%
domContentLoaded 1058.5 ms +7.41% -9.3% 1026.3 ms +0.34% -2.51% 3.14%
loaded 1059.48 ms +7.4% -9.28% 1027.48 ms +0.33% -2.54% 3.11%
firstContentfulPaint 452.86 ms +12.27% -38.98% 427.98 ms +2.4% -3.84% 5.81%
firstBlock 3801.43 ms +2.45% -2.27% 3762.58 ms +1.38% -2.14% 1.03%
type 18.17 ms +4.02% -9.41% 17.27 ms +0.87% -4.63% 5.21%
navigate 115.76 ms +5.87% -5.78% 108.03 ms +10.52% -7.27% 7.16%
loadPatterns 1107.73 ms +3.2% -2.6% 1021.6 ms +10.38% -7.28% 8.43%
loadPages 1052.31 ms +4.91% -5.06% 1093.36 ms +2.2% -2.57% -3.75%
wpTotal 310.79 ms +4.75% -6.86% 312.77 ms +2.18% -10.31% -0.63%
wpMemoryUsage 12.70 MB +0% -0% 12.66 MB +0% -0% 0.3%
wpDbQueries 44 +2.27% -0% 44 +2.27% -0% 0%

43b506e 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.

Regression: updating a specific option will only change its value and will not corrupt others in /test/e2e/specs/editor/various/core-settings.spec.js, passed after 1 failed attempt.
Error: expect(received).toStrictEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Array [
    "rewrite_rules",
-   "SERIALIZED DATA",
+   "",
  ]
    at forEach (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:46:26)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:43:35
keeps the blocks selected after dropping a multi-selection in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
TypeError: Cannot read properties of null (reading 'y')
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:824:57
    at dragSelectionBelowLanding (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:823:3)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:908:3
refuses the drop and uploads nothing in /test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 2
Received array:  [{"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=198"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/198", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-198", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-10-21T05:29:16", "date_gmt": "2026-10-21T05:29:16", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK-1.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK-1.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "10x10_e2e_test_image_z9T8jK-1.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK-1.png"}, "id": 198, "link": "http://localhost:8889/10x10_e2e_test_image_z9t8jk-2/", "media_details": {"file": "2026/10/10x10_e2e_test_image_z9T8jK-1.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-10-21T05:29:16", "modified_gmt": "2026-10-21T05:29:16", "ping_status": "closed", "post": null, "slug": "10x10_e2e_test_image_z9t8jk-2", "source_url": "http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK-1.png", "status": "inherit", "template": "", "title": {"rendered": "10x10_e2e_test_image_z9T8jK"}, "type": "attachment"}, {"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=194"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/194", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-194", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-10-21T05:28:59", "date_gmt": "2026-10-21T05:28:59", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "10x10_e2e_test_image_z9T8jK.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK.png"}, "id": 194, "link": "http://localhost:8889/10x10_e2e_test_image_z9t8jk/", "media_details": {"file": "2026/10/10x10_e2e_test_image_z9T8jK.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-10-21T05:28:59", "modified_gmt": "2026-10-21T05:28:59", "ping_status": "closed", "post": null, "slug": "10x10_e2e_test_image_z9t8jk", "source_url": "http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK.png", "status": "inherit", "template": "", "title": {"rendered": "10x10_e2e_test_image_z9T8jK"}, "type": "attachment"}]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js:71:45
returns focus to the edit button when the flyout is dismissed in /test/e2e/specs/site-editor/page-list.spec.js, passed after 1 failed attempt.
Error: expect(locator).toBeFocused() failed

Locator: getByRole('button', { name: 'Edit Status' })
Expected: focused
Timeout: 5000ms
Error: element(s) not found

Call log:
  - Expect "toBeFocused" getByRole('button', { name: 'Edit Status' }) with timeout 5000ms
  - waiting for getByRole('button', { name: 'Edit Status' })

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/site-editor/page-list.spec.js:540:32

43b506e Run

@aaronrobertshaw
aaronrobertshaw force-pushed the try/border-style-editor-defaults branch from b1a6bae to f52e9f8 Compare October 20, 2026 05:21
…me defaults

Replace the legacy :where([style*='border-...']) fallback in
block-library/common.scss with a cascade-aware, three-layer mechanism:

1. Edit-time intelligent default in the block-instance BorderPanel:
   when a user sets border color or width and no explicit or
   inherited (block-type or active block-style variation) style
   exists, persist border.style = 'solid' so saved attributes are
   self-describing.

2. Editor render-time fallback in useBlockProps: legacy unmigrated
   content (attributes carry color/width but no style) renders with
   visible borders in the editor wrapper.

3. Front-end render-time fallback via a render_block filter using
   WP_HTML_Tag_Processor: covers static and dynamic blocks, mirrors
   the JS cascade resolution.

The CSS fallback removal eliminates the entire false-positive class
that PR #77476 was patching around (inline styles whose substring
matches 'border-color' / 'border-width' incorrectly gaining a
forced solid style — e.g. background-image URLs, --border-color
custom properties).

Inheritance lookup covers block-type (styles.blocks[name].border)
and active block-style variation. Root styles.border is
intentionally excluded because it compiles to a body selector and
border-style is not a CSS-inherited property.

Tests:
- 32 JS unit tests (helpers + no-mutation).
- 28 PHPUnit tests mirroring the JS matrix plus
  gutenberg_get_variation_name_from_class and the render_block
  filter integration.
- 3 Playwright E2E specs covering the original #77476 regression
  cases and legacy-content render-time fallback.
…eedback

- Drop the `__experimentalBorder.style` support gate from the
  render_block fallback filter. Opting out of `style` block-support
  prevents users from picking a style via UI; it does not mean a
  block whose attributes carry a color or width should be rendered
  invisibly. The legacy CSS `:where()` rule this replaces did not
  gate on `style` support either, so the previous gate was a
  silent regression for blocks that opt into color/width only.

- Add a quick-exit in both the PHP render_block filter and the JS
  `useBlockProps` path when the block has no border-related
  attributes at all. Avoids registry / theme-json resolver work and
  preset-color resolution for the common case of border-supporting
  blocks without border data set.

- Move `@wordpress/i18n` import into the WordPress dependencies
  section, restoring the conventional ordering that the strict
  eslint config enforces.

- Add a PHPUnit test asserting the fallback fires for blocks
  registered with `__experimentalBorder: { color: true, width: true }`
  (no `style` sub-feature).
@aaronrobertshaw
aaronrobertshaw force-pushed the try/border-style-editor-defaults branch from f7d3f7d to 43b506e Compare October 21, 2026 05:17

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] Design Tools Tools that impact the appearance of blocks both to expand the number of tools and improve the experi Global Styles Anything related to the broader Global Styles efforts, including Styles Engine and theme.json [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant