ReferenceComponent utilities tokens
Navigator draws your app's destinations and lays out its Panes. Each level of the URL adds one pane, and nested layouts render them.
Navigation has four parts, and each is a component you type.
NavigatorThe app frame. You give it the current route.
Navigator.PrimaryThe top-level destinations
Navigator.SecondaryThe pages inside one destination
PaneTickets
PaneAn event
PaneA ticket
Navigator is the app frame. You give it the current route as value.Navigator.Primary holds the top-level destinations.Navigator.Secondary holds the pages inside one destination.Pane is a column of content.Navigator.Item is a destination, in either nav.
One URL level is one pane. /account/tickets is one pane, /account/tickets/muster-fest-glamping-4821 is two, and /account/tickets/muster-fest-glamping-4821/tk-9f2c1a is three. Roadie reads each pane's depth from the order the panes render in.
Everything else is automatic or opt-in.
Navigator.Menu instead of navigating.Navigator.SecondaryPane replaces it with your own.A shell has at most one list pane, any number of detail panes and at most one inspector. column says which one a pane is, and it defaults to detail. A shell with no list is normal. This docs site is a single detail pane.
There is no tertiary level. A secondary nav can't hold another one, and a pane at depth 2 is just a pane.
Each example adds one idea to the one before it. DemoRouter is a docs-only router. It keeps the path in state and gives it to the render function, the way a layout gets it from your framework. Press Full width to see the panes as columns, and narrow the window to see the tab bar and the stacked panes.
Two destinations and one pane that shows the current page.
Put a Navigator.Secondary inside Settings. Navigator generates its list pane, beside your page where two columns fit and behind it when panes stack. Your code still renders one pane.
Under Tickets, the path picks the panes. The list is always there, and an event adds a second pane after it. Choose an event, then use Back.
A ticket adds a third pane. Nothing declares a depth. Each pane takes the next one because it renders after the last. Choose an event, then a ticket.
Use nested layouts. Each route layout renders its own pane, then {children}. A URL renders exactly the panes its depth implies, whether you click to it or reload it.
app/account/
├─ layout.tsx the shell, with Navigator and its destinations
└─ tickets/
├─ layout.tsx the tickets pane, then {children}
├─ page.tsx null
└─ [event]/
├─ layout.tsx the event pane, then {children}
├─ page.tsx null
└─ [ticket]/
└─ page.tsx the ticket pane| URL | Panes |
|---|---|
/account/tickets | Tickets |
/account/tickets/muster-fest-glamping-4821 | Tickets, Muster Fest glamping |
/account/tickets/muster-fest-glamping-4821/tk-9f2c1a | Tickets, Muster Fest glamping, Bell tent for two |
The top layout is a client component. It passes usePathname() as value, so the nav and the panes read the same route. Mount RoadieLinkProvider once at the app root, so every href stays a client navigation.
A layout returns its pane and {children} in a fragment. The fragment adds no element, so every pane lands as a sibling of the others and Roadie reads the depth from their order. Wrap them in a div and the columns break.
A level whose pane comes from its layout still needs a page.tsx for the URL to exist. It returns null. This is the cost of nested layouts, one line and one comment per level.
// app/account/tickets/page.tsxexport default function TicketsPage() {return null // The tickets pane comes from layout.tsx.}
| Piece | Must be | Why |
|---|---|---|
The shell layout, Navigator, Navigator.Primary and everything inside it | Client | Navigator finds its parts by element reference. A server component replaces those references, so items, groups and menus silently disappear. |
Pane and everything in it | Server-safe | This is what lets each route layout stay a plain server component. |
Navigator.Primary must be a direct child of Navigator. Your own component around it hides it. See splitting a big tree.
An app with several trees, such as /user and /orgs, can share one shell. It takes a kind and a base path. Each rail is a function the shell calls, not a component it renders, so Navigator.Primary stays a direct child. Views take the same base path, so one tickets view serves both trees.
Roadie never reads the URL. To let a phone user open a destination's list, or More, from a link or with Back, keep a flag such as ?nav or ?more in the query string and pass it to showList and showMore. The Navigator reference covers the props.
Suspense boundary. useSearchParams client-renders a prerendered page up to the nearest one.window.location.search, so the page's own params survive the flag.depthAlmost never. Roadie derives depth from render order, on the server too. Pass it only when panes render out of order, as in a resumed partial prerender under Next's cacheComponents, or sibling Suspense boundaries that hydrate out of order.
Return the pane straight away and await inside it. The column and the header paint within about 40ms of the click, a skeleton stands in for the body, and the frame shows that it is waiting.
Do
Put the slow part in its own async component inside Pane.Body. The real header paints at once and the body shows the skeleton.
Don’t
Await in the page or layout before it returns the pane. Next holds the whole route, and nothing on screen changes until the data arrives.
params above the pane is fine. It resolves at once. The rule is about slow work.loading on the pane replaces the default skeleton with your own. loading on Pane.Body wins over it.Pane.Body still paints the column, with a skeleton header in place of yours.loading.tsx is optional. Next partially prefetches a dynamic route only when it has one. Without it, a click waits for the server's first bytes before anything changes, and the frame's pending indicator covers that gap.pending holds the pending indicator for a wait Roadie can't see, such as a client fetch without Suspense or a mutation.Roadie marks a click on any Roadie link. For a navigation you start yourself, such as a router.push, report it with usePendingNavigation, documented on Linking alongside useRoadieLink. useNavigatorSecondary lives on Navigator.
Navigator never reads the URL, so any router works, including none. Every live example on this site is this shape.
value and update it from onValueChange. A destination with an href also goes through your link, so both set the same path, which is harmless.RoadieLinkProvider your own link, so list rows and Back update the same state.A route renders only the panes it has reached. Client state can mount one early, such as a detail column that asks you to pick an event. Pass reached={false} on it. It fills its column where two fit, but it never becomes the top pane, so a phone shows the list.