Parallel Routes
ExperimentalParallel 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.
<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.
| Folder | Creates URL? | How it renders |
|---|---|---|
| @team | No - slot only | Independent SlotBoundary block |
| @team/page.tsx | Yes - via parent / | Renders when parent matches (e.g. /) |
| @analytics/page.tsx | Yes - 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.
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.
// Slot content for / - does not add /@analytics to the URL
export default function AnalyticsSlot() {
return <div>Analytics for /</div>
}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.
Example: @team/settings/page.tsx exists, but @analytics has no /settings route. Navigating to /settings renders the @team settings page plus @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 | @analytics | Renders |
|---|---|---|---|
| / | page.tsx | page.tsx | Main page + both slots |
| /settings | settings/page.tsx | default.tsx | @team settings + @analytics default |
| /unknown | no match | no match | Both slots -> default.tsx or No Content |
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 />.
@analytics has two subpages: page-views and visitors. A layout inside the slot shares the tab bar across them.
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.
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.
export default function Loading() {
return <div>Loading analytics...</div>
}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.
<Layout>
{children}
<SlotBoundary slot="team" /> A
<SlotBoundary slot="analytics" /> B
</Layout>| Path | URL | Support |
|---|---|---|
| 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-views | Slot sub-route |
| app/@analytics/layout.tsx | - | Nested layout with Outlet |
| app/_components/Header.tsx | - | Ignored (_ prefix) |
src/App.tsx.