Skip to content

BorderBoxControl : Remove orphaned color values when border width is cleared#75431

Closed
dpmehta wants to merge 2 commits intoWordPress:trunkfrom
dpmehta:fix/split-border-editor-frontend-mismatch
Closed

BorderBoxControl : Remove orphaned color values when border width is cleared#75431
dpmehta wants to merge 2 commits intoWordPress:trunkfrom
dpmehta:fix/split-border-editor-frontend-mismatch

Conversation

@dpmehta
Copy link
Copy Markdown

@dpmehta dpmehta commented Feb 11, 2026

What?

Fixes a mismatch between the editor and frontend when individual border widths are cleared in split border mode. The editor correctly shows only the intended border, but the frontend renders all four borders due to orphaned color values generating unwanted inline CSS.

Fixes #75415

Why?

When a block has a unified border (e.g., 2px solid black on all sides) and the user switches to split border mode and clears the width from individual sides, the color value persists even though the width has been removed.

This results in the saved block attributes containing color-only border sides:

{
  "border": {
    "top": { "width": "2px", "color": "var:preset|color|contrast" },
    "right": { "color": "var:preset|color|contrast" },
    "bottom": { "color": "var:preset|color|contrast" },
    "left": { "color": "var:preset|color|contrast" }
  }
}

These orphaned color values are then serialized into inline CSS like border-right-color, border-bottom-color, and border-left-color, without corresponding border-width declarations. Browsers render these with their default width, causing all four borders to appear on the frontend even though only the top border was intended.

How?

In onSplitChange() within BorderBoxControl's hook.ts, border sides are now sanitized before being passed up to the parent. If a border side has a color but no width, it is treated as empty (undefined), preventing orphaned color values from being saved to block attributes.

A border with only color and no width is not useful and should not be persisted.

Testing Instructions

  1. Add a Paragraph block
  2. Set a unified border (e.g., 2px width, black color)
  3. Click the unlink button to switch to individual border controls
  4. Clear the width from Right, Bottom, and Left borders
  5. Save the post and view on the frontend

Expected: Only the top border is visible on the frontend.

Before fix: All four borders appear because orphaned border-*-color declarations are rendered with default browser widths.

Screencast or Screenshot

Before fix :

border-issue.mov

After fix :

border-issue-fix.mov

@github-actions github-actions bot added [Package] Components /packages/components First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository labels Feb 11, 2026
@github-actions
Copy link
Copy Markdown

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @dpmehta! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@dpmehta dpmehta marked this pull request as ready for review February 11, 2026 16:58
@dpmehta dpmehta requested a review from ajitbohra as a code owner February 11, 2026 16:58
@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 11, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: dpmehta <mehtadev@git.wordpress.org>
Co-authored-by: talldan <talldanwp@git.wordpress.org>
Co-authored-by: andrewserong <andrewserong@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: aaronrobertshaw <aaronrobertshaw@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@talldan talldan added the [Type] Bug An existing feature does not function as intended label Feb 12, 2026
@talldan
Copy link
Copy Markdown
Contributor

talldan commented Feb 12, 2026

Thanks for working on this. The components package will need a changelog entry for this change before the PR can be merged.

It'd be great to see some unit tests added too!

@talldan
Copy link
Copy Markdown
Contributor

talldan commented Feb 12, 2026

Having tested, I think this is actually a fairly complex issue, so it might need some further input.

Firstly, I noticed the same issue exists not only for split borders, but also for the unified (single) border control.

But also I don't think clearing the color value when there's no width is the right approach to fixing this. Users should be able to set color or width independently. This allows for overriding only the color or the width when those values have been set somewhere else (for example in global styles, theme.json or in an external stylesheet).

So the bug here seems to be more that the editor doesn't display the border correctly.

I'll ping some other contributors for input (@aaronrobertshaw, @andrewserong, @tellthemachines, @ramonjd).

@dpmehta
Copy link
Copy Markdown
Author

dpmehta commented Feb 12, 2026

@talldan Thanks for your valuable input , I'd want to explain why I picked this strategy.

Users should be able to set color or width independently

So, if we look at current code for border styling, we can see that when the border width is set to zero, the color value for the border is cleared; this is the expected / actual behavior, which is already defined and can be seen in the reference section below.

if ( hasZeroWidth && ! hadPreviousZeroWidth ) {
// Before clearing the color and style selections, keep track of
// the current selections so they can be restored when the width
// changes to a non-zero value.
setColorSelection( border?.color );
setStyleSelection( border?.style );
// Clear the color and style border properties.
updatedBorder.color = undefined;
updatedBorder.style = 'none';
}


For that test case is also written which you can find below.

it( 'should clear color and set style to `none` when setting zero width', async () => {
const user = userEvent.setup();
const props = createProps();
render( <TestBorderControl { ...props } /> );
await openPopover( user );
await user.click( getColorOption( 'Green' ) );
await user.click( getButton( 'Dotted' ) );
await user.type( getWidthInput(), '0', {
initialSelectionStart: 0,
initialSelectionEnd: 1,
} );
expect( props.onChange ).toHaveBeenNthCalledWith( 3, {
color: undefined,
style: 'none',
width: '0px',
} );
} );

In the preceding reference, you can see that the color value is cleared when the border width value is zero, even you can see above test case for the same also written. so from above we can see its deliberate UX decision.

So clearing border width value ( which means its undefined ) and setting the width to zero are similar cases, so I thought the same solution should be used, because if we carefully observe existing behavior when the width value is zero, it clearly indicates that color value without valid border width value is not relevant or useful

@andrewserong
Copy link
Copy Markdown
Contributor

andrewserong commented Feb 12, 2026

Interesting problem!

This allows for overriding only the color or the width when those values have been set somewhere else (for example in global styles, theme.json or in an external stylesheet).

This. Keep in mind that it's also possible for folks to use the border block support with custom blocks that might already set explicit border width values, and for border width to be set in global styles. This is a crude example, but take this screenshot:

image

In this case, I've set a paragraph block in global styles to have an overall width of 15px, I then set an individual paragraph block's top border to 30px and each of its sides are red. Leaving an empty value for the individual sides' widths means that the value is simply empty. It isn't zero, and it doesn't clear anything out. The width is received from the global styles values.

So I think an empty field is a slightly different case to setting to 0. It can be tricky to make changes to these sorts of things because while fixing for one use case, it's easy to introduce a bug in another.

@dpmehta
Copy link
Copy Markdown
Author

dpmehta commented Feb 12, 2026

@andrewserong Thanks for your input.

While considering your case, I attempted to follow what you were saying, which is outlined below.

  • Set the width value to 10px and the border color to black from the global styles.

  • In the post editor, add a paragraph block and verify that global styles are being applied.

  • Then I tried overriding it with a 30px width value and black color for the unified border, which also worked as intended.

  • Then I cleared the 30px border width value (which was an override value) for the right, left, and bottom borders.

  • Verified that it was working as expected because we removed overriding values, so global styles should be applied. On the frontend and in the editor, we can see 10px as border width and black as border color for the right, left, and bottom borders, which is global styling that is applied correctly, as is expected when overriding values are removed.

If I understood correctly, you were concerned about global styling? Just wanted to confirm - this is already handled in the fix.

Below you can find Screencast for what i explained above :

border-styling.mp4

@andrewserong
Copy link
Copy Markdown
Contributor

andrewserong commented Feb 12, 2026

If I understood correctly, you were concerned about global styling?

Global styles is just one example, but the styles could come from anywhere. In your video it looks like you're using the same color in global styles as in the individual block instance. Try changing colors at the individual block level and you should see the issue (it's what I was trying to show in my screenshot above). I.e. if you set the individual block to red, and then adjust the widths of each side, the red should persist, but in this PR the red would get cleared out.

Hope that helps!

@ramonjd
Copy link
Copy Markdown
Member

ramonjd commented Feb 12, 2026

if you set the individual block to red, and then adjust the widths of each side, the red should persist, but in this PR the red would get cleared out.

Here's what I'm seeing (similar to what Andrew explains above) given the following scenario:

  • Global Styles says: "All paragraph borders should be 10px wide"
  • Individual paragraph block in a post: "I want red borders, but keep the 10px width from Global Styles"

This PR

Kapture.2026-02-13.at.10.49.26.mp4

Trunk

Kapture.2026-02-13.at.10.50.32.mp4

I think it'd be classed as a regression if users cannot change the color of a border in this scenario.

I don't have a solution, but the logic needs to account for inherited widths (e.g., from Global Styles/theme.json). If YES → output both color and inherited width and if NO → don't output anything (no orphaned color).

But as Andrew says too "the styles could come from anywhere".

@aaronrobertshaw
Copy link
Copy Markdown
Contributor

Thanks for raising this @dpmehta and everyone else for the detailed discussion 👍

The underlying bug discussed here, the styling discrepancy between the editor and frontend, is real and definitely worth fixing. However, as others have already flagged, I don't think the approach in this PR is the right one.

I've put up a simple fix as an alternative in #75546.

Here's a summary of what I found digging into this:

The component controls are working as intended

There's an important distinction between setting width to zero and clearing the width:

  • Width set to 0 is an explicit user action — they're deliberately making the border invisible. The existing onWidthChange logic correctly clears the color and style in this case (preserving them in state so they can be restored).
  • Width cleared (undefined) means the user is removing their override at the block instance level. It doesn't mean "no width" — it means "defer to the cascade." The width could be coming from theme.json, Global Styles, or a third-party stylesheet.

These are semantically very different. Stripping the color when width is cleared would silently discard the user's explicit color choice in cases where the width is inherited from elsewhere. Exactly the regression @andrewserong and @ramonjd demonstrated above.

The actual cause of the editor/frontend mismatch

The border fallback styles in packages/block-library/src/common.scss use CSS attribute selectors like [style*="border-right-color"] to apply border-style: solid when a border color is present in a block's inline style. This ensures borders are visible without the user needing to explicitly set a style.

On the frontend, the PHP style engine outputs individual longhand properties (border-top-color, border-right-color, etc.), so these selectors match correctly.

In the editor, when all four sides share the same color value, React merges the identical per-side properties into a single border-color shorthand in the DOM. The existing attribute selectors don't match border-color (only border-top-color, border-right-color, etc.), so border-style: solid is never applied, and the borders aren't visible in the editor — even though they will be on the frontend.

When the border is "linked" the styles use shorthand properties but the block also gets the .has-border-color class which does match the existing fallback styles.

The fix

The alternative PR (#75546) adds a [style*="border-color"] fallback rule alongside the existing individual side selectors in common.scss. This ensures the editor matches the frontend rendering when React merges the per-side colors into shorthand. It's a one-line CSS addition that addresses the root cause without modifying any component data handling.

I've added it to common.scss with the other fallbacks even though this is primarily only for the editor. The whole collection of fallbacks have a note that we could potentially refactor them to be applied directly via the block support etc.

Summary

As this PR would introduce a regression more prominent than the bug being addressed, I propose we close this and proceed with the fix in #75546.

@aaronrobertshaw
Copy link
Copy Markdown
Contributor

Thanks again for all the work and discussion here 🙏

As we've merged the fix in #75546, I'll close this PR.

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

Labels

First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Package] Components /packages/components [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Border width cleared for individual sides shows correctly in editor but renders full border on frontend

5 participants