Skip to content

feat(router-core): replay view transitions on browser Back/Forward#7697

Open
KurtGokhan wants to merge 3 commits into
TanStack:mainfrom
KurtGokhan:feat/replay-view-transition-on-traversal
Open

feat(router-core): replay view transitions on browser Back/Forward#7697
KurtGokhan wants to merge 3 commits into
TanStack:mainfrom
KurtGokhan:feat/replay-view-transition-on-traversal

Conversation

@KurtGokhan

@KurtGokhan KurtGokhan commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

💬 Discussion: #7698

Problem

View transitions only fire for navigations that go through commitLocation<Link viewTransition> or navigate({ viewTransition }), which set router.shouldViewTransition right before pushing to history. Browser Back/Forward arrives as popstate (the history subscriber is router.load, action BACK | FORWARD | GO) and never passes through commitLocation, so the flag stays unset and no transition plays.

Result: an app animates A → B via a Link, but pressing Back gives a hard cut. The only workaround today is defaultViewTransition, which animates every navigation — you can't opt one flow in and have it round-trip.

Solution

A new opt-in router option, default false:

createRouter({ replayViewTransitionOnTraversal: true })

The router records the view-transition value each entry was committed with (keyed by __TSR_index) and replays it on traversal:

  • PUSH/REPLACE: record the current shouldViewTransition for the arriving index (truthy values only, so it never short-circuits defaultViewTransition).
  • BACK/FORWARD/GO: before startViewTransition reads the flag in onReady, set
    shouldViewTransition = shouldViewTransition ?? recorded(leaving) ?? recorded(arriving).

Checking both endpoints makes replay symmetric — a transition opted into on A → B plays on both B → A (back) and a later A → B (forward) — and an explicitly-set value is never clobbered. It lives in router-core's load, the single point that sees the history action for every navigation kind, so no subscriber-ordering tricks are needed. No-op when off / on the server.

Why in-memory rather than history.state

ViewTransitionOptions.types can be a function, which is not structured-cloneable, so it can't be written to history.state. An in-memory Map preserves it by reference; the only cost is losing flags across a hard reload (degrades to no transition).

Open questions for maintainers

  • API namereplayViewTransitionOnTraversal, or fold into defaultViewTransition?
  • Default — the asymmetry is arguably a bug; should replay be default-on for navigations that already opted into a transition? This PR keeps it conservative (opt-in, independently shippable).

Tests

packages/router-core/tests/view-transition-traversal.test.ts: replay on back, replay on forward, plain edge → no transition, explicit value not clobbered, ViewTransitionOptions object (incl. functional types) preserved by identity, only the transitioned entry replays, and off-by-default.

Demo

StackBlitz: ts-router-view-transition-replay (NOT READY YET) — navigate Home → Details → About (each link animates), then use the browser Back/Forward buttons. Toggle the replayViewTransitionOnTraversal checkbox to compare: on = transitions replay, off = hard cut. (Best viewed in a Chromium browser.)

I also tested the feature in one of the examples and verified that the transition is direction aware. This required some changes on the example project and I pushed those too.

Google.Chrome-.Vite.App-000782.mp4

Docs & changeset

Added a replayViewTransitionOnTraversal entry to the router options docs and a changeset.

AI Usage Disclosure

I used AI for generating the changes in this PR, and most of the PR description. But the initial plan was mine and I reviewed the plan and every single line of code committed, and tried to steer AI away from making bad decisions.

Summary by CodeRabbit

  • New Features
    • Added an optional router setting to replay view transitions on browser Back/Forward traversal, preserving the original opted-in transition style instead of a hard cut.
    • Updated the React view-transitions examples to use a direction-aware transition so “previous/next” motion matches navigation intent.
  • Documentation
    • Documented the new replayViewTransitionOnTraversal option and clarified its behavior, defaults, and reload behavior.
  • Bug Fixes
    • Preserves default behavior when the setting is disabled, with no change to existing transitions.
  • Tests
    • Added coverage for replay behavior across Back/Forward traversal and edge cases.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f20b59f-1dbf-4918-b2ed-c0b3e38e0bc2

📥 Commits

Reviewing files that changed from the base of the PR and between 972f993 and d9f327a.

📒 Files selected for processing (5)
  • .changeset/replay-view-transition-on-traversal.md
  • docs/router/api/router/RouterOptionsType.md
  • examples/react/view-transitions/src/directionAwareTransition.ts
  • packages/router-core/src/router.ts
  • packages/router-core/tests/view-transition-traversal.test.ts
✅ Files skipped from review due to trivial changes (2)
  • .changeset/replay-view-transition-on-traversal.md
  • docs/router/api/router/RouterOptionsType.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/react/view-transitions/src/directionAwareTransition.ts
  • packages/router-core/src/router.ts
  • packages/router-core/tests/view-transition-traversal.test.ts

📝 Walkthrough

Walkthrough

Adds a router option to replay opted-in view transitions during browser Back/Forward traversal, stores transition state per history entry in memory, and updates the React view-transitions example to use a direction-aware helper.

Changes

Replay view transitions on traversal

Layer / File(s) Summary
Router option contract
packages/router-core/src/router.ts, docs/router/api/router/RouterOptionsType.md, .changeset/replay-view-transition-on-traversal.md
Adds replayViewTransitionOnTraversal to RouterOptions and documents the new option in the API reference and changeset.
Router traversal replay
packages/router-core/src/router.ts
Stores per-history-entry transition state, calls replay handling during load, and restores shouldViewTransition during traversal.
Traversal replay tests
packages/router-core/tests/view-transition-traversal.test.ts
Mocks document.startViewTransition, builds a router harness, and covers replay, overrides, functional types, disabled behavior, and default fallback behavior.
Direction-aware transition helper
examples/react/view-transitions/src/directionAwareTransition.ts
Adds PAGE_ORDER, pageRank(pathname), and slideByDirection for computing slide direction from route locations.
Example router and links
examples/react/view-transitions/src/main.tsx, examples/react/view-transitions/src/routes/*.tsx
Enables traversal replay in the example router and replaces static link transition objects with slideByDirection.

Sequence Diagram(s)

sequenceDiagram
  participant BrowserHistory
  participant RouterCore
  participant viewTransitionsByIndex
  participant document.startViewTransition
  BrowserHistory->>RouterCore: traversal history action
  RouterCore->>RouterCore: load()
  RouterCore->>RouterCore: recordOrReplayViewTransition(historyAction)
  RouterCore->>viewTransitionsByIndex: read or store transition by index
  RouterCore->>document.startViewTransition: replay opted-in transition
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

package: router-core, documentation

Poem

A bunny hopped by Back and then by Forward, too,
and found the same old slide transition, bright and new.
slide-left, slide-right—the ears all wiggle high,
while history remembers what hopped on by. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Clearly summarizes the main router-core change: replaying view transitions during Back/Forward traversal.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

KurtGokhan added a commit to KurtGokhan/tanstack-router that referenced this pull request Jun 25, 2026
Add the opt-in `replayViewTransitionOnTraversal` router option. The router
records the view-transition value each history entry was committed with (in
memory, keyed by __TSR_index) and replays it on BACK/FORWARD/GO, so a
transition opted into via <Link viewTransition> / navigate({ viewTransition })
no longer hard-cuts on browser Back/Forward. Includes tests, docs and a changeset.
@KurtGokhan KurtGokhan force-pushed the feat/replay-view-transition-on-traversal branch from f15254d to b640f6b Compare June 25, 2026 23:11
…ample

Enable replayViewTransitionOnTraversal in the view-transitions example and make
its links direction-aware via a page-order-based types function, so browser
Back/Forward replay the transition in the correct direction.
@KurtGokhan KurtGokhan marked this pull request as ready for review June 25, 2026 23:40

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
examples/react/view-transitions/src/directionAwareTransition.ts (1)

33-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap the early return in braces.

Line 34 violates the repo’s JS/TS control-flow style rule.

Suggested change
   types: ({ fromLocation, toLocation }) => {
-    if (!fromLocation) return ['slide-left']
+    if (!fromLocation) {
+      return ['slide-left']
+    }
     const from = pageRank(fromLocation.pathname)

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements. Never write one-line bodies like if (foo) x = 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/react/view-transitions/src/directionAwareTransition.ts` around lines
33 - 34, The early return in directionAwareTransition’s types callback uses a
one-line if without braces, which violates the repo’s control-flow style. Update
the conditional in the fromLocation check to use curly braces around the return
statement, matching the required JS/TS style used throughout the codebase and
keeping the logic in the same types function.

Source: Coding guidelines

packages/router-core/tests/view-transition-traversal.test.ts (1)

63-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the remaining documented traversal cases.

This suite never exercises history.go(), and it also doesn't lock down the “does not affect defaultViewTransition” contract. Since recordOrReplayViewTransition() handles GO in the same traversal branch and intentionally falls back to defaultViewTransition when nothing is recorded, a focused case for each would make the new contract much harder to regress.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/tests/view-transition-traversal.test.ts` around lines 63
- 189, Add test coverage in replayViewTransitionOnTraversal for the missing
traversal cases: exercise history.go() through the same traversal path used by
traverse(), and add a case that verifies recordOrReplayViewTransition() falls
back to defaultViewTransition when no recorded transition exists. Use the
existing router.navigate, traverse, and startViewTransitionSpy setup in this
suite so the GO branch and the defaultViewTransition contract are both
explicitly locked down.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/router-core/src/router.ts`:
- Around line 283-289: The option description in router.ts is too broad about
what is replayed on Back/Forward traversals; update the docs on the option and
the mirrored changeset text to say it only replays explicit per-navigation
`viewTransition` opt-ins from `<Link viewTransition>` or `navigate({
viewTransition })`, while traversals without that still use
`defaultViewTransition` via `startViewTransition()`. Keep the wording aligned
with the `defaultViewTransition` behavior and reference the relevant option
block in `router.ts` so the clarification stays consistent.

---

Nitpick comments:
In `@examples/react/view-transitions/src/directionAwareTransition.ts`:
- Around line 33-34: The early return in directionAwareTransition’s types
callback uses a one-line if without braces, which violates the repo’s
control-flow style. Update the conditional in the fromLocation check to use
curly braces around the return statement, matching the required JS/TS style used
throughout the codebase and keeping the logic in the same types function.

In `@packages/router-core/tests/view-transition-traversal.test.ts`:
- Around line 63-189: Add test coverage in replayViewTransitionOnTraversal for
the missing traversal cases: exercise history.go() through the same traversal
path used by traverse(), and add a case that verifies
recordOrReplayViewTransition() falls back to defaultViewTransition when no
recorded transition exists. Use the existing router.navigate, traverse, and
startViewTransitionSpy setup in this suite so the GO branch and the
defaultViewTransition contract are both explicitly locked down.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66f6fb48-390e-4f1e-b114-e92c53beda02

📥 Commits

Reviewing files that changed from the base of the PR and between bb2daa6 and 972f993.

📒 Files selected for processing (9)
  • .changeset/replay-view-transition-on-traversal.md
  • docs/router/api/router/RouterOptionsType.md
  • examples/react/view-transitions/src/directionAwareTransition.ts
  • examples/react/view-transitions/src/main.tsx
  • examples/react/view-transitions/src/routes/explore.tsx
  • examples/react/view-transitions/src/routes/how-it-works.tsx
  • examples/react/view-transitions/src/routes/index.tsx
  • packages/router-core/src/router.ts
  • packages/router-core/tests/view-transition-traversal.test.ts

Comment thread packages/router-core/src/router.ts
…ult-fallback tests

- Clarify in JSDoc/docs/changeset that without the option only per-navigation
  viewTransition opt-ins are not replayed; traversals still fall back to
  defaultViewTransition.
- Add curly braces to the example helper's early return (repo style) + docstring.
- Add tests for a multi-step history.go() traversal and the defaultViewTransition
  fallback contract.
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