Index Routes vs Layout Routes
After reading this you will be able to tell an index route from a layout route at a glance, declare each correctly in any mainstream router, and diagnose the two distinct blank screens that a confusion between them produces.
← Back to Nested & Layout Routing
Prerequisites
Core Concept
Both are children in a route tree. Both lack a path segment of their own. They do opposite jobs.
An index route is the default child: it fills the parent’s outlet when the URL stops at the parent. Its purpose is to answer “the user asked for /settings and nothing more — what goes in the panel?” It has no path because it is the parent’s path. Without one, visiting /settings renders the layout with an empty hole.
A layout route is a wrapper: it contributes a level of UI without contributing a URL segment. Its purpose is to give three routes a shared frame without inventing a /frame prefix. It has no path because it deliberately consumes none. Its children carry the real paths.
The confusion arises because in several routers the two are distinguished by a single flag or a naming convention rather than by structure. React Router uses index: true for one and an object with only element and children for the other. Next.js uses page.tsx for one and (group) folders for the other. SvelteKit uses +page.svelte versus a (group) directory. The declarations look nearly identical and the effects are unrelated.
The diagnostic is a single question: does this route need to render when the URL is exactly the parent’s path? If yes, it is an index route. If it needs to render for several different paths, it is a layout route.
Implementation
// TypeScript 5.x — both kinds in one table, React Router 6.22 shape
import { createBrowserRouter } from "react-router-dom";
export const router = createBrowserRouter([
{
// A LAYOUT route: no path, so it adds no URL segment. It exists purely to
// wrap the auth screens in a shared centred card.
element: <AuthFrame />,
children: [
{ path: "login", element: <Login /> },
{ path: "register", element: <Register /> },
{ path: "reset", element: <ResetPassword /> },
],
},
{
path: "settings",
element: <SettingsLayout />, // renders <Outlet /> beside its sidebar
children: [
// An INDEX route: fills the outlet when the URL is exactly "/settings".
{ index: true, element: <SettingsOverview /> },
{ path: "profile", element: <Profile /> },
{ path: "billing", element: <Billing /> },
],
},
]);
The same two ideas in the other mainstream routers:
// Vue Router 4.3 — index is a child with an empty path; layout is a pathless parent
const routes = [
{
// Layout route: no `path`, children carry the real paths.
component: AuthFrame,
children: [
{ path: "/login", component: Login },
{ path: "/register", component: Register },
],
},
{
path: "/settings",
component: SettingsLayout,
children: [
{ path: "", component: SettingsOverview }, // index
{ path: "profile", component: Profile },
],
},
];
// Angular 17 — pathMatch: "full" is what makes the empty path an index route
export const routes: Routes = [
{
// Layout route: a component with children and no path of its own.
component: AuthFrameComponent,
children: [
{ path: "login", component: LoginComponent },
{ path: "register", component: RegisterComponent },
],
},
{
path: "settings",
component: SettingsLayoutComponent,
children: [
// Without pathMatch: "full", an empty path matches EVERY child prefix.
{ path: "", pathMatch: "full", component: SettingsOverviewComponent },
{ path: "profile", component: ProfileComponent },
],
},
];
File-system routers encode the same distinction in directory names rather than configuration:
# Next.js App Router
app/
(auth)/ ← layout route: parentheses mean "no URL segment"
layout.tsx
login/page.tsx → /login
register/page.tsx → /register
settings/
layout.tsx ← renders {children}
page.tsx ← INDEX route: /settings
profile/page.tsx → /settings/profile
# SvelteKit 2
src/routes/
(auth)/ ← layout route: same parenthesis convention
+layout.svelte
login/+page.svelte → /login
settings/
+layout.svelte
+page.svelte ← INDEX route: /settings
profile/+page.svelte → /settings/profile
A third case sits between the two and is worth naming, because teams reach for a layout route when they actually want it. A pathless route with a loader but no element contributes neither UI nor a URL segment — it exists only to attach data or a guard to a group of siblings. React Router supports it as a route object with loader and children and no element; Angular expresses it with canActivate on a childless parent. It is the right tool when three screens share an authorisation check but no visual frame, and using a full layout route for that purpose adds an unnecessary component to every render.
Verification
// @playwright/test v1.44
import { test, expect } from "@playwright/test";
test("the parent path renders content, not an empty panel", async ({ page }) => {
await page.goto("/settings");
await expect(page.locator("[data-layout='settings']")).toBeVisible();
// The outlet must not be empty — this is the assertion a missing index fails.
await expect(page.locator("[data-panel]")).toBeVisible();
});
test("a layout route adds no URL segment", async ({ page }) => {
await page.goto("/login");
await expect(page).toHaveURL(/\/login$/); // no /auth/ prefix
await expect(page.locator(".auth-frame")).toBeVisible(); // the wrapper still rendered
});
A quick static check catches most cases before the browser does: every parent route that has children and is directly linked to anywhere in the application should have exactly one child with an empty path. Walking the route table in a unit test is faster and more reliable than remembering.
Two smaller checks are worth automating alongside it. Assert that no route with children also has an index flag, since a route cannot be both a parent and its own default child. And assert that every pathless route in the table has at least two children — a layout route wrapping a single child is almost always a mistake, either an index route that was mislabelled or a level of nesting that adds nothing.
Gotchas
- Angular’s empty path without
pathMatch: "full"matches every child prefix, so the index component renders on top of, or instead of, its siblings. It is the single most common Angular routing bug. - An index route cannot have children. It represents a terminal state; if you find yourself wanting children on it, you actually wanted a layout route.
- Two index routes under one parent is ambiguous. The second is unreachable, and most routers will not warn.
- Layout routes break naive breadcrumb generation. A crumb derived from chain position will invent an entry with no URL; generate crumbs only from routes that have a
path. - A redirect is not an index route. Redirecting
/settingsto/settings/profileworks but changes the URL, so the parent path stops being bookmarkable and the redirect shows up in analytics as an extra navigation.
FAQ
Can a route be both? No — one fills the parent’s own path, the other consumes no path at all. If you need both behaviours, use a layout route with an index child inside it.
Should /settings redirect to /settings/profile instead of having an index? Prefer the index route. A redirect discards the URL the user asked for, adds a history entry, and makes the parent path unusable as a shareable link.
Why do file-system routers use parentheses? A parenthesised directory groups files without contributing a segment — the file-system spelling of a layout route. app/(auth)/login/page.tsx serves /login, not /auth/login.
What renders if I omit both? The layout renders with an empty outlet. Nothing errors, which is why this is diagnosed as a data problem far more often than as a routing one.
Do index routes get their own loader? Yes, and they should. The overview screen usually needs different data from any of its siblings, and giving it a loader keeps the parent’s loader free of screen-specific concerns.
Related
- Nested & Layout Routing — the route tree these two route kinds live in.
- Nested Route Outlets Explained — the hole an index route exists to fill.
- Sharing Layout State Across Nested Routes — what a layout route can hand down to the children it wraps.