Parallel Routes

Experimental

Parallel routes with @ slots - independent route trees that render at the same time via SlotBoundary.

Overview

Parallel routes let you render multiple pages in the same view at once. Folders prefixed with @ define slots - named subtrees that resolve independently of the main route tree and do not add a segment to the URL. Each slot is scanned like a normal route and tagged with its slot name.

app
@team
Apage.tsx
@analytics
Bpage.tsx
layout.tsx
page.tsx
example.com
A
B
Component hierarchy
<Layout>
  {children}
  <SlotBoundary slot="team" /> A
  <SlotBoundary slot="analytics" /> B
</Layout>

Slots are generated as independent blocks via SlotBoundary. The main route and each slot match the current pathname on their own. Slot names must match /^[a-zA-Z][a-zA-Z0-9_-]*$/. This feature is experimental - check the generated src/App.tsx to see how slots are wired.

FolderCreates URL?How it renders
@teamNo - slot onlyIndependent SlotBoundary block
@team/page.tsxYes - via parent /Renders when parent matches (e.g. /)
@analytics/page.tsxYes - via parent /Renders at the same time as the main page

Convention: slots

slots use the @folder convention. The tree below defines two slots: @analytics and @team.

app
@analytics
page.tsx
@team
page.tsx
layout.tsx
page.tsx

Routes inside a slot support the same patterns as the main tree: dynamic segments [id], catch-alls [...slug], nested layouts, and templates. Each route is tagged with its slot name. Files prefixed with _ or . are ignored.

app/@analytics/page.tsx
// Slot content for / - does not add /@analytics to the URL
export default function AnalyticsSlot() {
  return <div>Analytics for /</div>
}
app/@team/page.tsx
export default function TeamSlot() {
  return <div>Team for /</div>
}

default.tsx fallback

When no route inside a slot matches the current URL, the nearest default.tsx is rendered (nearest-wins: a subfolder shadows an ancestor). If none exists in the chain, a built-in "No Content" fallback is used.

app
@team
settings
page.tsx
@analytics
default.tsx
page.tsx
default.tsx
layout.tsx
page.tsx

Example: @team/settings/page.tsx exists, but @analytics has no /settings route. Navigating to /settings renders the @team settings page plus @analytics/default.tsx.

app/@analytics/default.tsx
export default function Default() {
  return <div>Select analytics view</div>
}
default.tsx only applies inside @slot folders. Resolution is nearest-wins, same as loading.tsx, error.tsx, and not-found.tsx.

Behavior in Bini

Bini.js is a pure SPA with BrowserRouter. Matching order: static first, then dynamic :param, then required catch-alls *, then optional catch-alls **. Within each category, shorter paths win. Slots resolve independently through the same matchRoute() path used for API routes.

Current URL@team@analyticsRenders
/page.tsxpage.tsxMain page + both slots
/settingssettings/page.tsxdefault.tsx@team settings + @analytics default
/unknownno matchno matchBoth slots -> default.tsx or No Content
Open generated src/App.tsx to see how slots are wired with SlotBoundary. Manifest entries include a slotName field for tooling such as generateRouteManifest().

Tab groups inside slots

Add a layout.tsx inside a slot so that slot can navigate on its own (for example, tabs). Layouts render child routes with <Outlet />.

app
@analytics
page-views
page.tsx
visitors
page.tsx
layout.tsx
...

@analytics has two subpages: page-views and visitors. A layout inside the slot shares the tab bar across them.

app/@analytics/layout.tsx
import { Link, Outlet } from 'react-router-dom'

export default function AnalyticsLayout() {
  return (
    <>
      <nav className="flex gap-4 border-b">
        <Link to="/page-views">Page Views</Link>
        <Link to="/visitors">Visitors</Link>
      </nav>
      <Outlet />
    </>
  )
}

Loading and error boundaries

Each slot can define its own loading.tsx and error.tsx. They use nearest-wins and are code-split with React.lazy. In the diagram: A = loading for @team, B = error for @analytics, C = loading for @analytics.

...
@team
page.tsx
error.tsx
Aloading.tsx
@analytics
page.tsx
Berror.tsx
Cloading.tsx
layout.tsx
example.com
A
Loading...
C
Loading...
example.com
B
Error...

The loading file is the Suspense fallback. Error boundaries reset when the pathname changes. The built-in fallback renders nothing in development and a generic retry UI in production.

app/@analytics/loading.tsx
export default function Loading() {
  return <div>Loading analytics...</div>
}
app/@team/error.tsx
export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Team failed</h2>
      <button onClick={() => reset()}>Retry</button>
    </div>
  )
}

Complete example

Structure supported by bini-router for parallel routes: slots, default fallback, nested layouts inside slots, and loading/error boundaries. Slots render through independent SlotBoundary blocks - they are not passed as props into the layout.

app
@team
Apage.tsx
@analytics
Bpage.tsx
layout.tsx
page.tsx
example.com
A
B
Component hierarchy
<Layout>
  {children}
  <SlotBoundary slot="team" /> A
  <SlotBoundary slot="analytics" /> B
</Layout>
PathURLSupport
app/page.tsx/Main tree
app/@analytics/page.tsx/Slot via SlotBoundary
app/@team/page.tsx/Slot via SlotBoundary
app/@analytics/default.tsx-Fallback, nearest-wins
app/@analytics/page-views/page.tsx/page-viewsSlot sub-route
app/@analytics/layout.tsx-Nested layout with Outlet
app/_components/Header.tsx-Ignored (_ prefix)
For conditional UI inside a slot, gate rendering in the slot page itself. Confirm the generated composition in src/App.tsx.
Was this helpful?