Route Masking and URL Rewrites
Purpose and Scope
Route masking and URL rewrites solve related but distinct URL-shaping problems in TanStack Router. Route masking lets an app navigate to one internal route while persisting a different URL in the browser history and address bar. URL rewrites transform URLs in both directions between the browser-facing URL and the router-facing URL. Use this page when you need modal routes with shareable canonical URLs, alternate public URLs for existing routes, locale or tenant prefixes, legacy path migration, or deployment-level rewrite behavior that must stay aligned with the router’s route tree.
Sources: docs/router/guide/route-masking.md, docs/router/guide/url-rewrites.md
The key design constraint is that the route tree remains the application contract. A masked route or rewritten URL should not force you to duplicate route definitions just to support a different browser URL. Instead, masking stores a temporary runtime location in history state, while rewrites transform the URL before and after router interpretation. That keeps typed navigation, params, search validation, loaders, and route matching attached to the same internal route definitions while still giving users and hosts the public URLs they expect.
Sources: docs/router/guide/route-masking.md, docs/router/guide/url-rewrites.md
Relevant Source Files
docs/router/guide/route-masking.md- Defines route masking, explains the history-state mechanism, documents imperativemaskusage onLinkandnavigate, and introduces declarativerouteMasks.docs/router/guide/url-rewrites.md- Defines bidirectional URL rewrites, therewrite.inputandrewrite.outputrouter options,location.href,location.publicHref, and common patterns such as locale prefixes, subdomain routing, legacy URLs, and multi-tenant routing.docs/router/api/router/createRouteMaskFunction.md- DocumentscreateRouteMaskas a helper for creatingRouteMaskobjects that can be passed toRouterOptions.routeMasks.examples/react/location-masking/src/main.tsx- Provides a React example that importsLink,createRouteMask,createRouter,useNavigate,useRouterState, devtools, and Radix Dialog primitives to demonstrate masked modal navigation with loader-backed photo routes.
Conceptual Model
Route masking is best understood as a navigation-time illusion. The app navigates to the route it wants to render, such as a photo modal route, but writes a different URL to the browser, such as the canonical photo page. The guide calls out examples like navigating to /photo/5/modal while masking the address as /photos/5, hiding a search param such as ?showLogin=true, or displaying /settings while the internal location uses a modal search value. Masking is useful when the runtime UI state should be modal or contextual, but the URL should remain clean, shareable, or fallback-friendly.
Sources: docs/router/guide/route-masking.md
Under the hood, masking uses the browser location.state API rather than changing the route tree. The guide shows the browser location containing the public pathname, search, and hash, plus a state object with __tempLocation. When the router parses a history entry that contains location.state.__tempLocation, it uses that temporary location as the runtime location. The browser URL can therefore be /photos/5, while the router behaves as though it matched /photo/5/modal. The original browser URL is retained as location.maskedLocation so tooling such as devtools can detect and display masked state.
Sources: docs/router/guide/route-masking.md
URL rewrites operate at a different layer. They are not attached to one specific navigation, and they do not rely on a temporary runtime location. Instead, a router-level rewrite option can transform the URL coming from the browser before route matching, then transform the router’s internal URL before it is written back to the browser. The documentation names these two directions input rewrite and output rewrite. This is the right model for URL policies that apply broadly across the app, such as stripping /en before matching /about, mapping subdomains into route prefixes, or continuing to support legacy public paths.
Sources: docs/router/guide/url-rewrites.md
API Components
The imperative masking API is available through the same navigation object shape developers already use with Link and navigate. The route-masking guide states that the mask option accepts the same navigation options as normal navigation, including to, replace, state, and search. That matters because a mask is not a string-only escape hatch; it participates in Router’s typed navigation system. In TypeScript, invalid mask navigation objects produce type errors in the same spirit as invalid links or navigations, so masked modal routes can still use route params and search state safely.
Sources: docs/router/guide/route-masking.md
<Link
to='/photos/$photoId/modal'
params={{ photoId: 5 }}
mask={{
to: '/photos/$photoId',
params: { photoId: 5 },
}}
>
Open Photo
</Link>The declarative masking API is centered on router configuration. The API reference documents createRouteMask as a helper that accepts a required RouteMask options object and returns an object with the RouteMask type signature. The example creates photoModalToPhotoMask with routeTree, from: '/photos/$photoId/modal', to: '/photos/$photoId', and params: true, then passes that object into createRouter({ routeTree, routeMasks: [photoModalToPhotoMask] }). Use this form when a mapping should apply consistently, instead of being repeated on each link or navigate call.
Sources: docs/router/api/router/createRouteMaskFunction.md
import { createRouteMask, createRouter } from '@tanstack/react-router'
const photoModalToPhotoMask = createRouteMask({
routeTree,
from: '/photos/$photoId/modal',
to: '/photos/$photoId',
params: true,
})
const router = createRouter({
routeTree,
routeMasks: [photoModalToPhotoMask],
})For rewrites, the public router option is rewrite, with input and output functions. Each function receives a URL object. The docs state that a rewrite may mutate and return the same URL, return a new URL, return a full href string that can be parsed into a URL, or return undefined to skip the rewrite. The router exposes both location.href, described as the internal URL after input rewrite, and location.publicHref, described as the external URL after output rewrite. That distinction is important when debugging route matching versus address-bar behavior.
Sources: docs/router/guide/url-rewrites.md
const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => url,
output: ({ url }) => url,
},
})Execution Flow
A masked modal navigation begins when the user follows a link or calls navigate with both the real target route and a mask object. The router resolves the real target, including params and search, then stores that target as temporary location state while writing the mask location to the browser. On the next parse, the router sees __tempLocation and matches the runtime route instead of the public URL. If the URL is copied or reloaded without that state, the browser-facing URL can behave as the fallback route, which is why masking is well suited to modal overlays over canonical pages.
Sources: docs/router/guide/route-masking.md
A rewritten navigation begins earlier, when the router reads the current browser URL. The input rewrite transforms the browser URL into the internal URL before route matching and loader execution. Later, when Router generates or writes a URL, the output rewrite maps that internal URL back to the public form. This bidirectional flow is what allows a public /en/about?q=test URL to match an internal /about?q=test route without creating a duplicated /en route branch. The same pattern can support subdomain routing, tenant-specific addressing, custom schemes, and legacy path migrations.
Sources: docs/router/guide/url-rewrites.md
Example: Masked Photo Modal
The React location-masking example shows how these concepts appear in an application rather than only in isolated snippets. It imports Router primitives such as Link, Outlet, RouterProvider, createRootRoute, createRoute, createRouteMask, createRouter, useNavigate, and useRouterState, plus TanStackRouterDevtools and Radix Dialog. The example defines a root route with validateSearch for a typed optional modal object, a photos layout route with a fetchPhotos loader, and a fetchPhoto helper that throws a custom NotFoundError when an invalid photo id is requested.
Sources: examples/react/location-masking/src/main.tsx
That example also illustrates why masking is not just cosmetic. The UI has a normal photos route that can list photo thumbnails, but it also has modal behavior built with Dialog primitives. The root component reads router status through useRouterState({ select: (s) => s.status }) and renders a spinner while navigation is pending. Devtools are rendered in the root layout, which is useful because the guide notes that devtools can detect masked locations and show the actual URL. Together, the example demonstrates a route-aware modal experience that still benefits from loaders, pending state, errors, and typed navigation.
Sources: docs/router/guide/route-masking.md, examples/react/location-masking/src/main.tsx
Choosing Between Masking and Rewrites
Choose route masking when a specific navigation should render one route while displaying another URL. Modal routes are the clearest case: the internal route can represent overlay UI, data loading, and error boundaries, while the public URL can represent the canonical resource. Masking also fits temporary UI state that you do not want to expose as search params. Prefer imperative masks for one-off links and actions, and declarative routeMasks when the same internal-to-public mapping should be installed once at router creation and reused across the app.
Sources: docs/router/guide/route-masking.md, docs/router/api/router/createRouteMaskFunction.md
Choose URL rewrites when the browser-to-router transformation is a global URL policy. Locale prefixes, subdomain routing, legacy URL migration, multi-tenant applications, and custom URL schemes are all documented rewrite use cases because they affect how the router interprets URLs before a route is selected. For deployment rewrites, keep the hosting layer and router layer aligned: the host may need to serve the app for multiple public paths, while Router’s rewrite.input and rewrite.output keep the app’s internal route matching and generated public hrefs consistent. Debug by comparing location.href with location.publicHref.
Sources: docs/router/guide/url-rewrites.md
Compact Reference
| Need | Use | Key API | Notes |
|---|---|---|---|
| Show modal route while preserving a canonical URL | Route masking | Link or navigate with mask | Stores runtime target in location.state.__tempLocation. |
| Apply the same mask across many navigations | Declarative route mask | createRouteMask and RouterOptions.routeMasks | API reference returns a RouteMask object for router configuration. |
| Strip or add locale prefixes | URL rewrite | rewrite.input, rewrite.output | Input maps browser URL to internal URL; output maps internal URL to browser URL. |
| Support legacy or tenant URLs without duplicating routes | URL rewrite | URL transforms | Functions can return a mutated URL, a new URL, a href string, or undefined. |
| Debug public versus internal URL behavior | Location fields | location.href, location.publicHref, location.maskedLocation | Rewrites expose internal and public hrefs; masking preserves the browser URL as masked location. |
Next Steps
Start with masking if your problem is a route-level UI pattern such as a photo dialog, comments modal, or settings overlay. Add mask to the link or navigation first, then promote the mapping to createRouteMask if the same path relationship appears repeatedly. Start with rewrites if your problem is an address-space policy such as locale prefixes, subdomains, or old URLs. After implementing either feature, test direct visits, reloads, copied links, pending states, loaders, and error paths so the public URL and internal route contract stay predictable.