Skip to content

Carry an authored auto menu margin onto the navigation host - #896

Merged
borkweb merged 1 commit into
trunkfrom
fix/nav-menu-inline-margin
Aug 18, 2026
Merged

Carry an authored auto menu margin onto the navigation host#896
borkweb merged 1 commit into
trunkfrom
fix/nav-menu-inline-margin

Conversation

@borkweb

@borkweb borkweb commented Aug 18, 2026

Copy link
Copy Markdown
Member

What

A header authored as nav{display:flex} with .navlinks{margin:0 0 0 auto} puts its menu at the opposite end from the brand. The transformed page put it back at the start.

The authored rule was not lost. The class rides onto the navigation block and the stylesheet reaches the page — the rendered menu still takes gap and padding from the very same rule set, so only margin failed. Core's own navigation stylesheet owns the inner list:

.wp-block-navigation ul{margin-left:0}   /* specificity 0,1,1 */
.navlinks{margin:0 0 0 auto}             /* authored, 0,1,0 */

An authored margin can never win on that element. The block host is the flex item that actually moves, so that is where it has to be restated.

Emitting it as a block attribute is not available: core/navigation declares supports.spacing as blockGap only — checked in both vendored copies of its block.json — so a margin attribute would never serialize.

How

Collect the classes carried by promoted list-navigation hosts from the serialized output, then for each authored bare-class rule whose class sits on one of those hosts and whose inline-axis margin is auto, emit after author CSS:

.wp-block-navigation.blocks-engine-list-navigation.navlinks{margin-left:auto!important;margin-right:0!important}

This is the mechanism the file already uses to reassert display:flex on the same host. The selector is self-limiting — it matches only an element that is both a promoted list navigation and carries the authored class.

Two deliberate limits:

  • Only auto is carried. That is the declaration which positions a flex item and the one core's list reset destroys. An authored length is left to the author rule, which core does not contest on the host.
  • The class must sit on a navigation host. Matching any bare-class rule with an auto margin made real projects emit .wrap{margin:0 auto} (a page wrapper) and .brand{margin-right:auto} (a brand) as navigation rules — dead CSS on 54 corpus documents. Reading the classes off the wp:navigation attributes took that to 10.

Verified in a render, not by reasoning

Re-rendered through the same run-fixture-matrix pipeline the promotion gate uses, WordPress 7.0.2, with only the transformer swapped. From the captured DOM:

source before after
menu margin-left 665.547px 0px 665.547px
menu x 965.55 300 965.55
items x 965.55 / 1037.08 / 1102.47 965.55 / 1037.08 / 1102.47

Blast radius

10 of 269 corpus design documents gain CSS. No document changes block markup — this is a stylesheet-only change. Unlike the neighbouring navigation fixes this one does not get to claim "nothing changed" as a safety argument, so the render is the evidence.

Known gaps

  • What zeroes margin-left on the navigation root specifically is still unidentified. The inner-list override is proven; the root's is not. !important sidesteps that question rather than answering it.
  • The fixture's menu still sits ~3.8px lower vertically than the source (y 30 vs 26.2). Unrelated to inline-axis alignment and not addressed here, but the promotion gate runs at --pixel-threshold 0, so that fixture can still register a difference.

Testing

  • php tests/unit/navigation-inline-margin-carry.php — 5 assertions, 2 fail against trunk. Covers the carry, the opposite side riding along, a page wrapper producing no rule, an authored length producing no rule, and a plain menu producing no rule.
  • php tests/parity/run.php — 278 fixtures pass
  • php tests/contract/run.php — pass
  • composer test — exit 0

No fixture was modified and no threshold was relaxed.

…ion host

## Summary
Restate an authored inline-axis `auto` margin on the promoted navigation block after author CSS, so a menu the author pushed to the far end of its landmark still renders there.

## Why
A header authored as `nav{display:flex}` with `.navlinks{margin:0 0 0 auto}` puts its menu at the opposite end from the brand. The transformed page put it back at the start.

The authored rule was not lost. The class rides onto the navigation block and the stylesheet reaches the page — the rendered menu still takes `gap` and `padding` from the very same rule set, so only `margin` failed. Core's own navigation stylesheet owns the inner list: `.wp-block-navigation ul{margin-left:0}` is specificity 0,1,1 and outranks the authored `.navlinks` at 0,1,0. An authored margin can never win on that element.

The block host is the flex item that actually moves, so that is where the authored margin has to be restated. Emitting it as a block attribute is not available: `core/navigation` declares `supports.spacing` as `blockGap` only, in both vendored copies of its `block.json`, so a margin attribute would never serialize.

## How
Collect the classes carried by promoted list-navigation hosts from the serialized output, then for each authored bare-class rule whose class sits on one of those hosts and whose inline-axis margin is `auto`, emit `.wp-block-navigation.blocks-engine-list-navigation.<class>{margin-…:…!important}` after author CSS. This is the mechanism the file already uses to reassert `display:flex` on the same host.

Two deliberate limits:

- **Only `auto` is carried.** That is the declaration which positions a flex item and the one core's list reset destroys. An authored length is left to the author rule, which core does not contest on the host.
- **The class must sit on a navigation host.** Matching any bare-class rule with an auto margin made real projects emit `.wrap{margin:0 auto}` (a page wrapper) and `.brand{margin-right:auto}` (a brand) as navigation rules — dead CSS on 54 corpus documents. Reading the classes off the `wp:navigation` attributes took that to 10.

The emitted selector is self-limiting: it matches only an element that is both a promoted list navigation and carries the authored class.

## Verified in a render
Re-rendered through the same `run-fixture-matrix` pipeline the promotion gate uses, WordPress 7.0.2. In the captured DOM the source menu computes `margin-left: 665.547px` at `x=965.55`; the transformed menu now computes `margin-left: 665.547px` at `x=965.55`. Individual items match too: 965.55 / 1037.08 / 1102.47 on both sides.

## Blast radius
10 of 269 corpus design documents gain CSS. **No document changes block markup** — this is a stylesheet-only change. Unlike the neighbouring navigation fixes, "nothing changed" is not available as a safety argument here, so the render is the evidence.

## Known gap
What zeroes `margin-left` on the navigation *root* specifically is still unidentified — the inner-list override is proven, the root's is not. `!important` sidesteps that question rather than answering it.

## Testing
- [ ] `cd php-transformer && php tests/unit/navigation-inline-margin-carry.php` — 5 assertions; 2 fail against trunk
- [ ] `cd php-transformer && php tests/parity/run.php` — 278 fixtures
- [ ] `cd php-transformer && php tests/contract/run.php`
- [ ] `cd php-transformer && composer test`
@borkweb
borkweb merged commit b1d7a4b into trunk Aug 18, 2026
6 of 7 checks passed
@borkweb
borkweb deleted the fix/nav-menu-inline-margin branch August 18, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant