Skip to content

Block states: pair focus states with a focus-within rule - #81268

Open
amitraj2203 wants to merge 1 commit into
trunkfrom
fix/81256-navigation-link-pseudo-state-selector
Open

Block states: pair focus states with a focus-within rule#81268
amitraj2203 wants to merge 1 commit into
trunkfrom
fix/81256-navigation-link-pseudo-state-selector

Conversation

@amitraj2203

@amitraj2203 amitraj2203 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What?

Closes #81256

Pairs a block's :focus and :focus-visible state styles with a rule that matches focus inside the styled element, so focus states apply to Navigation Link — on the front end as well as in the editor.

Why?

Block state styles are generated against the block's style root. core/navigation-link declares no selectors.root, so its state CSS targets the block wrapper — the <li>:

.wp-states-9d155b7a:hover { … }
.wp-states-9d155b7a:focus { … }   /* never matches */

:hover and :active match an element while a descendant of it is hovered or activated, so those states work: hovering the link hovers the item. Focus does not propagate that way — it matches only the element that received focus, the <a> inside. So :focus and :focus-visible on a Navigation Link had no effect at all, in the editor or on the front end.

core/button is unaffected because its selectors.root (.wp-block-button .wp-block-button__link) already points at the element that receives focus. That is the inconsistency reported in the issue.

How?

For each focus state, emit a second rule with the equivalent "focus is inside me" selector — :focus-within for :focus, :has(:focus-visible) for :focus-visible:

  • lib/block-supports/states.php: new gutenberg_get_state_styles_with_focus_pairs(), applied in the pseudo-state and responsive pseudo-state loops of gutenberg_render_block_states_support().
  • packages/block-editor/src/hooks/style.js: the same map in getPseudoStateCSSRules(), for the editor's block-instance state CSS.

:hover and :active are left alone — they already match while a descendant is hovered or activated. The paired rules are emitted separately, not as a selector list, so a browser without :has() support still honours the plain focus state. For blocks whose state styles already target the focusable element, the paired selector matches the same element and changes nothing. No block selectors change, so Global Styles output is untouched.

Screen.Recording.2026-08-06.at.12.41.58.PM.mov

Testing Instructions

  1. Add a Navigation block with a Custom Link, and a Button block, to a page.
  2. On the link, use the state control in the block card header to set Hover → red text, 40px, and Focus → blue text, 40px. Set the same on the Button. Return the state control to Default.
  3. With the Navigation block selected, hover the link on the canvas → hover style applies. Click into the link's text → focus style applies. On trunk the focus style does nothing.
  4. Save and view the page. Hover the link → hover style. Tab to the link → focus style applies. On trunk it never does.
  5. Confirm the Button behaves exactly as on trunk.
  6. Regression check: in Styles → Blocks → Custom Link, set a plain (Default state) text colour and confirm it still applies on the front end.

Testing Instructions for Keyboard

Step 4 is the keyboard path: Tab through the navigation until the styled link receives focus and confirm the configured focus style applies. In the editor, move the caret into the link's text with the keyboard and confirm the focus style previews.

@github-actions

github-actions Bot commented Aug 6, 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: amitraj2203 <amitraj2203@git.wordpress.org>
Co-authored-by: tellthemachines <isabel_brison@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>
Co-authored-by: jordesign <jordesign@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] Block editor /packages/block-editor label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Size Change: 0 B

Total Size: 7.82 MB

compressed-size-action

@amitraj2203 amitraj2203 self-assigned this Aug 6, 2026
@amitraj2203 amitraj2203 added the [Type] Bug An existing feature does not function as intended label Aug 6, 2026
@amitraj2203
amitraj2203 marked this pull request as draft August 6, 2026 07:24

@tellthemachines tellthemachines left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! However, I don't think we should try to fix this with a generic approach such as this. We may want to add focus-within as a pseudo state in its own right in the future for more complex blocks like tabs or cards, and this would complicate that needlessly. Also, for other blocks that may adopt the focus state we can't be sure this behaviour is always desirable.

It would be less risky to try a Navigation-specific fix for this. We have precedent for adding block-specific responsive styles in Gallery block; for this case a similar solution should work well.

@github-actions github-actions Bot added the [Package] Block library /packages/block-library label Aug 10, 2026
@amitraj2203

Copy link
Copy Markdown
Contributor Author

@tellthemachines
Thanks for the review, that makes sense, so I've dropped the generic change.

I tried the Gallery-style approach locally: Navigation Link's render callback builds its own focus rules and registers them with the style engine, scoped to the link inside the item. That part works on the front end.

One issue though. Generating the CSS in the block means redoing the bits the shared states code already handles, and I missed four of them on the first pass:

  • border-style: solid when a border width or colour is set without a style
  • background-image: unset when a state sets a solid background over a gradient
  • text-align, which the style engine doesn't output
  • the height / min-height resets that go with aspect-ratio

None of those are reachable through Navigation Link's supports today (it only supports typography), but the block now carries its own copy of that pipeline, and the editor would need a second copy in JS. That feels like a lot to maintain
for what is really just a wrong selector.

So would you be open to a per-block selector instead? Navigation Link would declare that its state styles target .wp-block-navigation-item__content, and the shared code would keep generating them exactly as it does now. Nothing changes for any block that doesn't declare it, and no :focus-within is involved, so it stays out of the way of adding that as a state later.


On the editor side: hovering a link previews the hover style fine, but clicking it doesn't preview the focus style. Clicking puts focus on the editable label inside the link, so the styled element never matches :focus. Button works because its RichText renders the .wp-block-button__link element itself — the styled element is the focused one — which Navigation Link can't do, since its label is nested inside the link.

So the editor needs to reflect focus some other way. I have a version that tracks focus in the block's subtree and applies the styles while focus is inside, so the generated CSS carries no focus pseudo-class at all and :focus-within stays free. Would you rather I went that way, or do you have a preferred mechanism?

Happy to go either way — just wanted to check before redoing it.

@github-actions github-actions Bot removed the [Package] Block editor /packages/block-editor label Aug 10, 2026
@amitraj2203
amitraj2203 marked this pull request as ready for review August 10, 2026 06:22
@github-actions github-actions Bot added [Package] Data /packages/data [Package] A11y /packages/a11y [Package] Autop /packages/autop [Package] Blob /packages/blob [Package] Compose /packages/compose labels Aug 10, 2026
Block state styles are generated against the block's style root, which for
a Navigation Link is the `<li>` wrapper. `:hover` and `:active` match an
element while a descendant of it is hovered or activated, so those states
work on the wrapper. Focus does not: it matches only the element that
received focus, the `<a>` inside. `:focus` and `:focus-visible` therefore
had no effect at all on the front end.

The block's render callback now builds its own focus state rules, scoped to
the link, and registers them with the style engine under an instance class —
the pattern the Gallery block uses for its responsive block gap. `:hover`
and `:active` are left to the states block support and emit no extra rule.
@amitraj2203
amitraj2203 force-pushed the fix/81256-navigation-link-pseudo-state-selector branch from 2ab0e68 to 38a77fc Compare August 10, 2026 06:28
@github-actions github-actions Bot removed [Package] Data /packages/data [Package] A11y /packages/a11y [Package] Autop /packages/autop [Package] Blob /packages/blob [Package] Compose /packages/compose [Package] Core data /packages/core-data [Package] API fetch /packages/api-fetch [Package] Components /packages/components [Package] Blocks /packages/blocks [Package] Block editor /packages/block-editor [Package] Data Controls /packages/data-controls [Package] Base styles /packages/base-styles [Package] Commands /packages/commands [Package] Core commands /packages/core-commands labels Aug 10, 2026
@t-hamano

Copy link
Copy Markdown
Contributor

Let's consider fixing this issue in a minor release, as 7.1 RC3 is scheduled for release tomorrow.

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

Labels

[Package] Block library /packages/block-library [Type] Bug An existing feature does not function as intended

Projects

Status: 🐛 Punted to 7.1.1

Development

Successfully merging this pull request may close these issues.

Navigation Block: pseudo-state (Hover / Focus) preview in Editor is inconsistent to other Elements with psuedo-states

3 participants