Skip to content

[fragment-scroll] Stop focusing the first focusable host descendant#89903

Merged
eps1lon merged 1 commit intocanaryfrom
sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant
Feb 18, 2026
Merged

[fragment-scroll] Stop focusing the first focusable host descendant#89903
eps1lon merged 1 commit intocanaryfrom
sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant

Conversation

@eps1lon
Copy link
Copy Markdown
Member

@eps1lon eps1lon commented Feb 12, 2026

Flagged behind experimental.appNewScrollHandler

focus() on Fragment refs works different than findDOMNode(this).focus().

findDOMNode will return an actual host element so it's effectively HTMLElement.focus(). If the element isn't focuseable, focus() will do nothing. By spec, there's also no way to determine if an element is focusable. It's up to user agents (e.g. sometimes scrollable containers are considered focusable).

FragmentInstance.focus will focus the first focusable descendant. That could be a focusable element deep within the fragment e.g. <Fragment><section><p>A lot of text</p><a href="">Next page</a></Fragment>. Focusing such a deep element may sometimes be counter productive.

Now we just blur the active element. This is how a hard navigation already works today and ensures we don't skip past content of the new segment.

This means you potentially stay a long way before the actual Segment (e.g. in a preceding nav) with the focus cursor which could be annoying. We could be clever and temporarily insert a focusable element at the start of the Segment, focus it, and then immediately remove. That would leave the focus cursor right before the Segment. However, this requires knowledge about the parent content (can we just insert an element of our choosing?) and relies on knowing what's focusable (a button would be a good bet). That's a bit too clever for a fix without concrete motivation so I'm waiting somebody to complain about the new behavior.

@nextjs-bot
Copy link
Copy Markdown
Collaborator

nextjs-bot commented Feb 12, 2026

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 455ms ▆▁▁▁▇
Cold (Ready in log) 438ms 439ms ▆▁▁▁▇
Cold (First Request) 1.262s 1.250s ▇▁▃▃▇
Warm (Listen) 456ms 456ms ▆▁▁▁▇
Warm (Ready in log) 448ms 446ms ▆▁▁▁▇
Warm (First Request) 352ms 346ms ▆▂▁▁▆
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 559ms 559ms ▁▁▁▁▁
Cold (Ready in log) 556ms 558ms ▅▆▃▆▆
Cold (First Request) 2.643s 2.658s ▄▃▂▅▄
Warm (Listen) 560ms 560ms ▁▁▁▁▁
Warm (Ready in log) 559ms 559ms ▅▅▂▅▅
Warm (First Request) 2.679s 2.671s ▃▃▂▄▃

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 4.032s 3.966s ▆▂▁▁▇
Cached Build 3.980s 4.001s ▆▂▁▁▇
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 18.384s 18.350s ▂▁▁▄▁
Cached Build 18.425s 18.425s ▂▁▁▄▁
node_modules Size 472 MB 472 MB ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **398 kB** → **398 kB** ⚠️ +13 B

80 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 761 B 768 B
Total 761 B 768 B ⚠️ +7 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 452 B 451 B
Total 452 B 451 B ✅ -1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.47 kB N/A -
6280-HASH.js gzip 57 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.53 kB N/A -
e8aec2e4-HASH.js gzip 62.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 255 B 253 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.53 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.48 kB -
6948ada0-HASH.js gzip N/A 62.6 kB -
9544-HASH.js gzip N/A 57.8 kB -
Total 231 kB 231 kB ⚠️ +726 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.5 kB 2.5 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.97 kB 7.97 kB ✅ -2 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 126 kB 125 kB
page.js gzip 250 kB 251 kB
Total 376 kB 376 kB ⚠️ +323 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 617 B 611 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.3 kB 43.7 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 44.9 kB 45.3 kB ⚠️ +394 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 3.9 MB 3.91 MB 🔴 +15.1 kB (+0%)
index.pack gzip 104 kB 103 kB
index.pack.old gzip 105 kB 102 kB 🟢 2.39 kB (-2%)
Total 4.1 MB 4.12 MB ⚠️ +12 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 316 kB 316 kB
app-page-exp..prod.js gzip 168 kB 168 kB
app-page-tur...dev.js gzip 316 kB 316 kB
app-page-tur..prod.js gzip 168 kB 168 kB
app-page-tur...dev.js gzip 312 kB 312 kB
app-page-tur..prod.js gzip 166 kB 166 kB
app-page.run...dev.js gzip 313 kB 313 kB
app-page.run..prod.js gzip 166 kB 166 kB
app-route-ex...dev.js gzip 70.5 kB 70.5 kB
app-route-ex..prod.js gzip 49.1 kB 49.1 kB
app-route-tu...dev.js gzip 70.6 kB 70.6 kB
app-route-tu..prod.js gzip 49.1 kB 49.1 kB
app-route-tu...dev.js gzip 70.1 kB 70.1 kB
app-route-tu..prod.js gzip 48.9 kB 48.9 kB
app-route.ru...dev.js gzip 70.1 kB 70.1 kB
app-route.ru..prod.js gzip 48.8 kB 48.8 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.2 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.2 kB 43.2 kB
pages-api.ru..prod.js gzip 32.8 kB 32.8 kB
pages-turbo....dev.js gzip 52.5 kB 52.5 kB
pages-turbo...prod.js gzip 38.4 kB 38.4 kB
pages.runtim...dev.js gzip 52.5 kB 52.5 kB
pages.runtim..prod.js gzip 38.4 kB 38.4 kB
server.runti..prod.js gzip 63.6 kB 63.6 kB
Total 2.8 MB 2.8 MB ⚠️ +2 B

@eps1lon eps1lon force-pushed the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch from e5cacc9 to 8ab833c Compare February 12, 2026 12:59
@eps1lon eps1lon force-pushed the sebbie/08-27-fix_scroll_restoration_bugs branch from 11678e6 to 36ea303 Compare February 12, 2026 12:59
@nextjs-bot
Copy link
Copy Markdown
Collaborator

nextjs-bot commented Feb 12, 2026

Tests Passed

@eps1lon eps1lon force-pushed the sebbie/08-27-fix_scroll_restoration_bugs branch from 36ea303 to 907aee1 Compare February 12, 2026 13:32
@eps1lon eps1lon force-pushed the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch from 8ab833c to 0577f90 Compare February 12, 2026 13:32
@eps1lon eps1lon force-pushed the sebbie/08-27-fix_scroll_restoration_bugs branch 2 times, most recently from fe5af8e to bf8dc72 Compare February 12, 2026 13:55
@eps1lon eps1lon force-pushed the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch from 0577f90 to 383063f Compare February 12, 2026 13:55
@eps1lon eps1lon changed the base branch from sebbie/08-27-fix_scroll_restoration_bugs to graphite-base/89903 February 13, 2026 15:11
@eps1lon eps1lon force-pushed the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch from 383063f to dcbed22 Compare February 13, 2026 15:11
@eps1lon eps1lon force-pushed the graphite-base/89903 branch from bf8dc72 to b42b301 Compare February 13, 2026 15:11
@eps1lon eps1lon changed the base branch from graphite-base/89903 to sebbie/02-12-_test_current_behavior_of_focus_after_next/link_navigation February 14, 2026 13:34
@eps1lon eps1lon force-pushed the sebbie/02-12-_test_current_behavior_of_focus_after_next/link_navigation branch 2 times, most recently from 1d8d157 to da3c183 Compare February 14, 2026 13:43
@eps1lon eps1lon force-pushed the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch 2 times, most recently from b5646be to 11080c4 Compare February 17, 2026 18:05
@eps1lon eps1lon force-pushed the sebbie/02-12-_test_current_behavior_of_focus_after_next/link_navigation branch from da3c183 to 5213184 Compare February 17, 2026 18:05
@eps1lon eps1lon force-pushed the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch from 11080c4 to f07c7b7 Compare February 17, 2026 18:36
@eps1lon eps1lon marked this pull request as ready for review February 17, 2026 18:58
@eps1lon eps1lon requested a review from unstubbable February 17, 2026 18:58
@eps1lon eps1lon requested a review from ztanner February 17, 2026 18:58
@eps1lon eps1lon force-pushed the sebbie/02-12-_test_current_behavior_of_focus_after_next/link_navigation branch from 5213184 to 88f7c75 Compare February 17, 2026 20:11
@eps1lon eps1lon force-pushed the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch from f07c7b7 to 219dfe3 Compare February 17, 2026 20:11
// We could workaround this by moving focus to a temporary element in
// the body. But adding elements might trigger layout or other effects
// so it should be well motivated.
activeElement.blur()
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

VoiceOver won't do any announcement on blur. Since this won't actually move the internal focus cursor, we may just drop the blur. Stopping to focus is more important.

Copy link
Copy Markdown
Member Author

eps1lon commented Feb 18, 2026

Merge activity

  • Feb 18, 5:26 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Feb 18, 5:28 PM UTC: Graphite rebased this pull request as part of a merge.
  • Feb 18, 5:50 PM UTC: @eps1lon merged this pull request with Graphite.

@eps1lon eps1lon changed the base branch from sebbie/02-12-_test_current_behavior_of_focus_after_next/link_navigation to graphite-base/89903 February 18, 2026 17:26
@eps1lon eps1lon changed the base branch from graphite-base/89903 to canary February 18, 2026 17:26
@eps1lon eps1lon force-pushed the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch from 219dfe3 to fce5986 Compare February 18, 2026 17:27
@eps1lon eps1lon merged commit 5509e1b into canary Feb 18, 2026
160 checks passed
@eps1lon eps1lon deleted the sebbie/02-12-_fragment-scroll_stop_focusing_the_first_focusable_host_descendant branch February 18, 2026 17:50
@github-actions github-actions bot added the locked label Mar 5, 2026
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants