Skip to content

Global Styles: explore a cascade popover for inherited-value overrides - #80993

Draft
JosVelasco wants to merge 1 commit into
WordPress:trunkfrom
JosVelasco:try/global-styles-origin-cascade
Draft

Global Styles: explore a cascade popover for inherited-value overrides#80993
JosVelasco wants to merge 1 commit into
WordPress:trunkfrom
JosVelasco:try/global-styles-origin-cascade

Conversation

@JosVelasco

@JosVelasco JosVelasco commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Important note

This is an exploration, opened as a draft to keep the conversation going. It is not proposed for merge as-is. It builds directly on the discussion in #80649 and is meant as something concrete to react to — take it over, cherry-pick from it, or close it.

What?

Follow-up exploration to #80649. That PR settles on nothing at rest, a diamond on override, with the diamond as a status indicator that performs no action and shows "Overrides inherited styles" on hover.

This keeps that model and asks what happens if the indicator becomes a way in rather than a dead end. Activating it opens a popover showing the property's whole cascade — every layer that contributed, the losing ones struck through — with a Clear action beside the value it would restore.

SIZE
  Custom CSS                7rem      ← in effect
  J̶u̶s̶t̶ ̶t̶h̶i̶s̶ ̶b̶l̶o̶c̶k̶             3̶r̶e̶m̶
  S̶u̶b̶t̶i̶t̶l̶e̶ ̶s̶t̶y̶l̶e̶        1̶.̶5̶ ̶–̶ ̶1̶.̶7̶5̶r̶e̶m̶
  S̶i̶t̶e̶-̶w̶i̶d̶e̶               1̶.̶1̶8̶7̶5̶r̶e̶m̶
  Clear

Everything stays behind the gutenberg-global-styles-inheritance-ui experiment added in #80815.

styles.mp4

Why?

Two threads from #80649 pointed here.

@mirka, on why the infotip pattern was chosen over a plain tooltip:

the infotip pattern will indeed be necessary when the popup wants to contain anything more than plain text. It just happens that this instance still doesn't. […] As we continue enhancing this inheritance indicator feature, the infotip popover can contain richer layouts, and maybe even links or buttons to edit the parent control it's inheriting from.

@t-hamano, on a problem with the status-only indicator:

for color-based UIs with inherited styles, this reset button will become unavailable.

with two options offered: add a reset action to the indicator, or restore the reset button beside it. This explores the first, which he noted "deviates from the original purpose of this PR" — hence a separate draft rather than a push to that branch. Note @jasmussen preferred restoring the button, so this takes a side in a live disagreement.

Beyond that, a one-bit marker can say overridden but not what was overridden, what the value would fall back to, or why the control disagrees with the canvas. Those are the questions that send people to the browser inspector.

How?

Engine. resolveStyle retains the full per-path cascade rather than only the winning layer. The ordered contributions array was already built and then collapsed during the merge; this records what it discarded, as cascade: Record<path, CascadeEntry[]>. sources is unchanged, so nothing downstream is affected. Layers carry structural metadata (block name, element, variation) rather than a pre-built string, so the UI composes and translates the labels — the engine stays i18n-agnostic.

Labels describe scope rather than structure, so reading top to bottom narrows: Site-wide → All Paragraph blocks → Subtitle style → Just this block → Custom CSS.

Two things the cascade alone could not explain:

  • A block's own custom CSS overrides a property without ever touching its style path. It now appears as its own cascade layer, parsed from style.css (top-level declarations only; nested selectors target other states).
  • Everything outside the Global Styles data model — theme stylesheets, plugin styles, Customizer CSS — is invisible to the engine, which excludes css outright via TREE_STRUCTURAL_KEYS. Rather than assert a winner the page contradicts, the popover compares the winning value against the element's computed style and says so when they disagree.

Also adopted from the #80649 review:

  • Dropped the at-rest dotted-underline treatment (nothing at rest).
  • Larger diamond — @joedolson noted the 6px square was indistinguishable from a dot, @t-hamano diagnosed the SVG being squashed, @jasmussen wanted it larger still. Here it is an 8px square (11.3px diagonal) rendering at its natural size.
  • Accessible names identify the property — "Where does Font size come from?" — per @mirka's suggestion, rather than one generic string repeated on every indicator.

Testing Instructions

  1. Enable Global Styles inheritance UI on the Gutenberg → Experiments screen.
  2. Add a Paragraph. Set a font size in the inspector — a diamond appears beside the control.
  3. Activate the diamond. The popover shows the cascade: your value over the inherited one, with Clear.
  4. For a deeper cascade, apply a block style variation (Subtitle) that also sets that property.
  5. Add font-size: 7rem !important under Advanced → Custom CSS. It appears as its own layer, and the diamond shows even when custom CSS is the only override.
  6. Add a rule to the theme or another plugin that wins on that property. The popover reports that something outside Global Styles is changing it.

Known gaps

Stated plainly, since this is a draft:

  • The mismatch check needs per-property verification. Authored values are not always what gets emitted: fluid typography rewrites a font size into a clamp(), and custom CSS bypasses that pipeline entirely. Both produced false positives before being handled. The property allowlist excludes the obviously messy cases but has not been audited property by property; spacing.* and border.* in particular are unverified.
  • The check runs when the popover mounts, not continuously, so a popover held open across a stylesheet change can go stale.
  • Naming the property in the accessible name makes getByRole('button', { name: /Color/ }) ambiguous, since it now matches both the control and its indicator. One existing test needed anchoring. Any contributor querying controls by property name will hit this.
  • The colour indicator is still hover-revealed (opacity: 0 at rest), inherited from when that slot held a reset button. Every other property signals its override at rest; colour does not.
  • The title attribute used for full values is not keyboard or touch accessible; it wants a real Tooltip.
  • No CHANGELOG.md entries, deliberately — this is not proposed for merge in its current shape.

AI disclosure

  • AI assistance: Yes
  • Tool(s) used: Claude Code (Opus 5)
  • What AI generated: the implementation, tests, and this description.
  • What was human-reviewed: direction, UX decisions, and behaviour were driven and reviewed interactively against a running editor throughout. Several defects were found only by that review — the indicator escaping its row after a CSS deletion, a diamond rendered unreachable underneath the contrast warning, an indicator that never appeared when custom CSS was the sole override, and the false positives above. Treat the code as exploratory: it demonstrates the idea and has not had the scrutiny a merge-ready patch needs.

Exploration, not proposed for merge as-is. Builds on the override-indicator
work in WordPress#80649 and stays behind the `gutenberg-global-styles-inheritance-ui`
experiment added in WordPress#80815.

Where WordPress#80649 marks an override with a diamond and an "Overrides inherited
styles" tooltip, this makes the indicator a way in: activating it opens a
popover showing the property's whole cascade — every layer that contributed,
the losing ones struck through — with a Clear action beside the value it
would restore.

Engine: `resolveStyle` retains the full per-path cascade rather than only the
winning layer. The ordered `contributions` array was already built and then
collapsed during the merge; this records what it discarded. `sources` is
unchanged, so nothing downstream is affected. Layers carry structural
metadata (block name, element, variation) instead of a pre-built string, so
the UI composes and translates the labels.

UI: origin labels describe scope rather than structure — "Site-wide", "All
Paragraph blocks", "Subtitle style", "Just this block", "Custom CSS".

Two things the cascade alone could not explain:

- A block's own custom CSS overrides a property without touching its style
  path, so it appears as its own cascade layer rather than as an unexplained
  discrepancy.
- Everything outside the Global Styles data model (theme stylesheets, plugin
  styles, Customizer CSS) is invisible to the engine. The popover compares
  the winning value against the element's computed style and says so when
  they disagree, rather than asserting a winner the page contradicts.

Also drops the at-rest dotted-underline treatment, matching the "nothing at
rest, something on override" direction settled in WordPress#80649, and adopts the
review feedback there: a larger diamond (the 6px square read as a dot) and
accessible names that identify the property.
@github-actions github-actions Bot added the [Package] Block editor /packages/block-editor label Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Block editor /packages/block-editor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant