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.

An index route compared with a layout route Two route trees. In the first, the settings parent has an index child that renders when the URL is exactly slash settings. In the second, a pathless layout route wraps three sibling routes that each carry their own path, contributing UI without a URL segment. Index route — fills the outlet at the parent path path: "settings" renders the layout index: true matches /settings exactly /settings layout + index no blank panel Layout route — UI without a URL segment no path renders a shared frame /login /register /reset the wrapper adds no segment to any of these URLs The diagnostic question Does it need to render when the URL is exactly the parent's path? Yes → index. Several different paths? → layout.
Both are pathless children; one answers the parent's own URL, the other wraps siblings that answer their own URLs.

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
The two blank screens and what each one means Two failure cases side by side. A missing index route renders the layout with an empty outlet at the parent path. A layout route mistakenly given a path adds an unwanted segment so every child URL gains a prefix and existing links break. Missing index route settings header — renders fine sidebar outlet is empty looks like a data bug Symptom: /settings shows chrome and no content. Fix: add a child with an empty path (index). Layout route given a path intended: /login actual: /auth/login every bookmark, email link and search result now 404s Symptom: URLs gain a segment nobody asked for. Fix: remove the path; wrap without contributing a segment.
A missing index route empties the panel; a layout route with a path silently rewrites every child URL.

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 /settings to /settings/profile works but changes the URL, so the parent path stops being bookmarkable and the redirect shows up in analytics as an extra navigation.
How each router spells an index route and a layout route A table of five routers showing the syntax for an index route and for a layout route in each. The two are distinguished by a flag, an empty path, or a parenthesised directory depending on the router. Declaring both kinds in five routers index route layout route React Router 6 index: true an object with no path Vue Router 4 path: "" a parent with no path Angular 17 path: "", pathMatch: "full" a component with children, no path Next.js App Router page.tsx (group) directory SvelteKit 2 +page.svelte (group) directory
The concepts are identical everywhere; only the spelling changes — which is exactly why the two get confused.

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.