Skip to content

UI: Defend anchor text-decoration against global CSS - #82620

Closed
CGastrell wants to merge 1 commit into
trunkfrom
fix/ui-global-css-defense-text-decoration
Closed

CGastrell wants to merge 1 commit into
trunkfrom
fix/ui-global-css-defense-text-decoration

Conversation

@CGastrell

Copy link
Copy Markdown
Contributor

What?

Adds a text-decoration bridge to @wordpress/ui's global CSS defense, so a Link's underline survives host pages that strip anchor underlines.

Why?

Link's default variant never declared an underline — it tunes offset, thickness and colour and relies on the UA underline. An unlayered host rule like .some-admin-page a { text-decoration: none; } therefore removes it outright and leaves the tuning styling nothing. global-css-defense.module.css already bridges outline, color, box-shadow, border-radius and transition for this reason; text-decoration was missing because wp-admin's own CSS never touches it.

The concrete case is the Jetpack plugin, which sets exactly that rule on every admin screen. It's the only one I can point to, but the shape is generic.

How?

A new .a:any-link block defends the three text-decoration longhands, with the initial values as fallbacks — so the defense alone changes nothing, and components opt in through the custom properties. Link bridges its values in on default / neutral. Link unstyled, LinkButton and Menu.LinkItem need no override.

Two things worth flagging:

  • All three longhands, because a host text-decoration shorthand resets colour and thickness too. Defending only the line restores an underline in the wrong colour and weight.
  • :any-link is a specificity increase, (0,1,0) → (0,2,0). The existing .a beats wp-admin's bare a selectors but loses to the wrapper-scoped .some-page a rules hosts actually write. The cost: a consumer styling a wp-ui link from outside at (0,1,0) will now lose and would need to set --_gcd-a-text-decoration-line. Keeping plain .a specificity avoids that but fixes nothing. Happy to take the other trade if preferred.

Testing Instructions

  1. Render a default Link on a page that also loads an unlayered .wrap a { text-decoration: none; }. On trunk the underline is gone; on this branch it's restored, with from-font thickness and the neutral stroke colour.
  2. On a page without such a rule, confirm Link (both tones and variants), LinkButton and Menu.LinkItem are unchanged from trunk. This is the important check.
  3. Confirm the external-link arrow stays un-underlined.

Testing Instructions for Keyboard

No interaction change. Tab to a Link and confirm the focus ring behaves as on trunk.

Screenshots or screencast

A Notice.ActionLink (a Link, tone="neutral", variant="default") on a page carrying .jetpack-pagestyles a { text-decoration: none; }.

Before After
gb-link-before gb-link-after

Computed styles on that element:

Before After
text-decoration-line none underline
text-decoration-thickness auto from-font
text-decoration-color rgb(30, 30, 30) (currentColor) rgb(141, 141, 141) (--wpds-color-stroke-interactive-neutral)

The host page runs the published @wordpress/ui, so the "after" is this PR's CSS injected with the built module hashes substituted, declarations otherwise verbatim.

Use of AI Tools

Researched and drafted with Claude Code, reviewed by me before submission. The cascade behaviour, specificity comparison and three-longhand requirement were each verified in a browser across trunk/branch × clean/hostile CSS, rather than reasoned about on paper.

@CGastrell CGastrell self-assigned this Sep 8, 2026
@CGastrell
CGastrell requested a review from a team as a code owner September 8, 2026 20:01
@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

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: CGastrell <cgastrell@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>

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

@github-actions github-actions Bot added the [Package] UI /packages/ui label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

📦 Bundle size

Size Change: +1.74 kB (+0.02%)

Total Size: 8.01 MB

📦 View Changed
Filename Size Change
build/modules/boot/index.min.js 57.6 kB +64 B (+0.11%)
build/scripts/block-editor/index.min.js 479 kB +304 B (+0.06%)
build/scripts/block-library/index.min.js 367 kB +89 B (+0.02%)
build/scripts/components/index.min.js 267 kB +56 B (+0.02%)
build/scripts/customize-widgets/index.min.js 59.1 kB +73 B (+0.12%)
build/scripts/edit-site/index.min.js 379 kB +249 B (+0.07%)
build/scripts/edit-widgets/index.min.js 66.9 kB +80 B (+0.12%)
build/scripts/editor/index.min.js 584 kB +324 B (+0.06%)
build/scripts/format-library/index.min.js 40.4 kB +89 B (+0.22%)
build/scripts/media-utils/index.min.js 202 kB +357 B (+0.18%)
build/scripts/patterns/index.min.js 12.4 kB +59 B (+0.48%)

1ef3f20 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 2a11036 trunk % Change
timeToFirstByte 60.8 ms +5.02% -10.03% 63.75 ms +1.8% -13.8% -4.63%
largestContentfulPaint 96 ms +4.17% -4.17% 92 ms +4.35% -6.52% 4.35%
lcpMinusTtfb 32.95 ms +22.61% -3.34% 30.7 ms +2.44% -4.07% 7.33%
wpBeforeTemplate 28.6 ms +20.42% -7.8% 33.55 ms +4.56% -19.91% -14.75%
wpTemplate 24.04 ms +2.45% -1.79% 24.38 ms +3.86% -3.77% -1.39%
wpTotal 56.22 ms +4.59% -9.69% 58.69 ms +2.25% -13.61% -4.21%
wpMemoryUsage 7.52 MB +0% -0% 7.48 MB +0% -0% 0.46%
wpDbQueries 17 +0% -0% 17 +0% -0% 0%

front-end-classic-theme

Metric 2a11036 trunk % Change
timeToFirstByte 37.6 ms +20.21% -2.13% 41.1 ms +18.13% -1.7% -8.52%
largestContentfulPaint 80 ms +7.5% -2.5% 86 ms +9.3% -2.33% -6.98%
lcpMinusTtfb 41.75 ms +2.51% -3.95% 44.1 ms +4.99% -1.47% -5.33%
wpBeforeTemplate 24.48 ms +26.8% -3.68% 23.77 ms +20.61% -1.64% 2.99%
wpTemplate 10.23 ms +5.77% -2.44% 14.27 ms +4.41% -3.08% -28.31%
wpTotal 34.81 ms +19.19% -2.67% 38.01 ms +18.23% -1.95% -8.42%
wpMemoryUsage 5.40 MB +0% -0% 5.47 MB +0% -0% -1.32%
wpDbQueries 10 +0% -0% 14 +0% -0% -28.57%

media-processing

Metric 2a11036 trunk % Change
mediaProcessingJpeg 413.54 ms +1.01% -0.64% 413.95 ms +0.77% -0.92% -0.1%
mediaProcessingAvif 6138.58 ms +0.36% -0.13% 6218.51 ms +0.08% -0.14% -1.29%
mediaProcessingJpegToAvif 4310.13 ms +0.15% -0.17% 4319.77 ms +0.48% -0.12% -0.22%

media-upload

Metric 2a11036 trunk % Change
jpegUploadProcessing 1486.46 ms +29.77% -4.21% 1426.62 ms +1.09% -0.79% 4.19%
pngUploadProcessing 214.02 ms +7.56% -5.75% 204.49 ms +7.72% -1.17% 4.66%
largeJpegUploadProcessing 1426.23 ms +0.32% -0.84% 1410.56 ms +1.4% -1.05% 1.11%
multipleImageUploadProcessing 1545.77 ms +1.58% -0.37% 1557.06 ms +0.36% -1.22% -0.73%

post-editor

Metric 2a11036 trunk % Change
serverResponse 523.22 ms +2.22% -10.8% 503.66 ms +3.2% -3.49% 3.88%
firstPaint 198.74 ms +12.74% -7.05% 208.05 ms +33.37% -10.13% -4.47%
domContentLoaded 1277.47 ms +1.31% -1.07% 1263.6 ms +1.92% -1.26% 1.1%
loaded 1278.62 ms +1.31% -1.07% 1264.79 ms +1.92% -1.26% 1.09%
firstContentfulPaint 424.77 ms +1.99% -2.37% 412.05 ms +4.32% -3.91% 3.09%
firstBlock 3514.4 ms +0.98% -0.47% 3489.29 ms +0.45% -0.84% 0.72%
type 14.1 ms +6.95% -4.18% 14.31 ms +8.74% -5.52% -1.47%
typeWithoutInspector 14.16 ms +7.91% -4.1% 13.58 ms +5.74% -6.41% 4.27%
typeWithTopToolbar 17.36 ms +8.18% -3.8% 17.85 ms +3.92% -6.39% -2.75%
typeContainer 8.25 ms +11.88% -4.12% 9.04 ms +8.74% -11.5% -8.74%
focus 75.02 ms +20.7% -2.88% 77.7 ms +8.16% -7.41% -3.45%
firstFocus 172.2 ms +0% -0% 171.7 ms +0% -0% 0.29%
selectAll 610.11 ms +1.47% -4.89% 596.67 ms +1.03% -5.35% 2.25%
listViewOpen 69.11 ms +6.54% -14.76% 68.47 ms +21.72% -8.91% 0.93%
inserterOpen 23.9 ms +4.85% -11% 22.95 ms +10.5% -14.73% 4.14%
inserterHover 3.5 ms +16.86% -9.71% 3.51 ms +8.26% -5.98% -0.28%
inserterSearch 7.45 ms +13.56% -7.92% 7.57 ms +20.74% -3.7% -1.59%
loadPatterns 612.73 ms +3.09% -5.38% 627.32 ms +2.44% -4.95% -2.33%
wpTotal 513.09 ms +2.19% -10.98% 493.95 ms +3.04% -3.59% 3.87%
wpMemoryUsage 13.05 MB +0% -0% 13.02 MB +0% -0% 0.28%
wpDbQueries 54 +0% -1.85% 54 +0% -0% 0%

site-editor

Metric 2a11036 trunk % Change
serverResponse 524.93 ms +3.44% -2.8% 531.64 ms +1.99% -9.13% -1.26%
firstPaint 290.1 ms +23.48% -23.7% 246.17 ms +10.89% -2.16% 17.85%
domContentLoaded 1424.39 ms +2.57% -0.5% 1440.49 ms +0.36% -0.3% -1.12%
loaded 1425.65 ms +2.59% -0.49% 1441.94 ms +0.36% -0.3% -1.13%
firstContentfulPaint 455.69 ms +3.36% -2.14% 453.67 ms +1.24% -3.35% 0.45%
firstBlock 4584.56 ms +0.74% -0.38% 4570.57 ms +0.5% -0.42% 0.31%
type 15.81 ms +7.53% -9.93% 16.4 ms +3.05% -13.54% -3.6%
navigate 109.23 ms +1.3% -8.24% 110.58 ms +2.3% -5.33% -1.22%
loadPatterns 1443.04 ms +3.19% -9.04% 1568.08 ms +3.66% -15.7% -7.97%
loadPages 1127.45 ms +1.94% -0.95% 1126.22 ms +2.39% -4.1% 0.11%
wpTotal 515.57 ms +3.37% -2.96% 521.69 ms +2.09% -9.28% -1.17%
wpMemoryUsage 12.06 MB +0% -0% 12.03 MB +0% -0% 0.3%
wpDbQueries 43 +2.33% -0% 43 +2.33% -0% 0%

1ef3f20 Run

`Link`'s default variant never declared an underline. It tuned
`text-underline-offset`, `text-decoration-thickness` and
`text-decoration-color` and relied on the user agent underline, so an
unlayered host rule such as `.plugin-admin-page a { text-decoration: none; }`
removed the underline and left the tuning styling nothing.

Defend `text-decoration-line`, `text-decoration-color` and
`text-decoration-thickness` on the `.a` global CSS defense, and bridge
`Link`'s own values into them. All three longhands are needed because a host
`text-decoration` shorthand resets each of them.

The new block is scoped to `:any-link`. The existing `.a` block is `(0,1,0)`,
which beats wp-admin's bare `a` selectors but loses to the wrapper-scoped
`.some-page a` rules that strip underlines in practice. `:any-link` raises it
to `(0,2,0)` and matches only the elements that carry a user agent underline.

Fallbacks are the initial values, so `Link` `unstyled`, `LinkButton` and
`Menu.LinkItem` need no override and nothing renders differently on a page
with no hostile global CSS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CGastrell
CGastrell force-pushed the fix/ui-global-css-defense-text-decoration branch from ae9911f to 1ef3f20 Compare September 8, 2026 20:05
@mirka

mirka commented Sep 9, 2026

Copy link
Copy Markdown
Member

I'm afraid the global CSS defense module is only for wp-admin's unlayered common.css / forms.css. It is not a shield against arbitrary plugin rules like .jetpack-pagestyles a { text-decoration: none; }. Those stay the consumer's problem.

Overscoped rules like.jetpack-pagestyles a { text-decoration: none; } are going to be problematic in a modern component-based architecture anyway, so I would recommend addressing that at the root. If you don't actually mean to do what that rule will do, the rule should be changed to match what you actually mean. That could be by layering that rule appropriately, or adding another selector so sections of the app can opt-in/opt-out.

And even if we were to try addressing this in the global CSS defense module as proposed, it would mean that the style would now win over any .my-class a overrides, not just Jetpack. Just because a one consumer wrote .jetpack-pagestyles a { text-decoration: none; } and didn't mean it, we can't assume that every consumer who writes that doesn't mean it.

I agree that Link is missing text-decoration: underline and we can add that for completeness. But it needs to remain a layered rule like the others.

@ciampo ciampo added the [Type] Enhancement A suggestion for improvement. label Sep 9, 2026
CGastrell added a commit to Automattic/jetpack that referenced this pull request Sep 16, 2026
…se (#52404)

The underline workaround said WordPress/gutenberg#82620 would narrow it.
That PR's defense only covers wp-admin's own unlayered styles, not plugin
rules like `.jetpack-pagestyles a`, so nothing upstream will retire this.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
matticbot pushed a commit to Automattic/jetpack-storybook that referenced this pull request Sep 16, 2026
…se (#52404)

The underline workaround said WordPress/gutenberg#82620 would narrow it.
That PR's defense only covers wp-admin's own unlayered styles, not plugin
rules like `.jetpack-pagestyles a`, so nothing upstream will retire this.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

Committed via a GitHub action: https://github.com/Automattic/jetpack/actions/runs/35133303437

Upstream-Ref: Automattic/jetpack@8d1d0f6
@mirka

mirka commented Sep 22, 2026

Copy link
Copy Markdown
Member

Closing for clean up.

@mirka mirka closed this Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] UI /packages/ui [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants