Child and Nested Routes
Nested routes (also called child routes) let you build hierarchical UI structures that match your URL hierarchy. A parent route renders a layout shell (header, sidebar, tabs) and an inner <router-outlet> displays the active child route's content.
This is the standard pattern for feature areas: /users → user list, /users/42 → user profile, /users/42/settings → user settings — all within a shared layout.
Defining Child Routes
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'users',
component: UsersShellComponent, // renders the layout + <router-outlet>
children: [
{ path: '', component: UserListComponent }, // /users
{ path: ':id', component: UserDetailComponent }, // /users/42
{ path: ':id/edit', component: UserEditComponent }, // /users/42/edit
{ path: ':id/posts', component: UserPostsComponent }, // /users/42/posts
],
},
{ path: '', redirectTo: '/users', pathMatch: 'full' },
];The Parent Layout Shell
The parent component (UsersShellComponent) renders the shared chrome and a <router-outlet> where child components appear.
// users-shell.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="users-layout">
<aside>
<nav>
<a routerLink="/users" routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }">All Users</a>
</nav>
</aside>
<main>
<!-- Child route components render here -->
<router-outlet />
</main>
</div>
`,
})
export class UsersShellComponent {}Deeper Nesting (Three Levels)
export const routes: Routes = [
{
path: 'org',
component: OrgShellComponent, // /org — org layout
children: [
{ path: '', component: OrgHomeComponent },
{
path: ':orgId',
component: OrgDetailShellComponent, // /org/acme — org detail layout
children: [
{ path: '', component: OrgOverviewComponent },
{ path: 'members', component: OrgMembersComponent },
{
path: 'projects',
component: ProjectsShellComponent, // /org/acme/projects — projects layout
children: [
{ path: '', component: ProjectListComponent },
{ path: ':id', component: ProjectDetailComponent },
],
},
],
},
],
},
];children must have a <router-outlet> in its template. Without it, child components have nowhere to render and will be silently ignored.Component-Less Parent Routes
Sometimes you want to group routes and apply shared guards or resolvers without an actual shell component. Use a component-less route with just a path and children.
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{
path: 'admin',
// No component — just provides a path prefix and shared guard
canActivate: [authGuard],
children: [
{ path: '', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent },
{ path: 'config', component: AdminConfigComponent },
],
},
];Lazy-Loaded Nested Routes
Feature areas are commonly lazy-loaded. The parent route uses loadChildren pointing to a file that exports the child routes array.
// app.routes.ts
export const routes: Routes = [
{
path: 'products',
loadChildren: () =>
import('./products/products.routes').then(m => m.PRODUCT_ROUTES),
},
];
// products/products.routes.ts
import { Routes } from '@angular/router';
export const PRODUCT_ROUTES: Routes = [
{
path: '',
component: ProductsShellComponent,
children: [
{ path: '', component: ProductListComponent },
{ path: ':id', component: ProductDetailComponent },
{
path: ':id/reviews',
loadComponent: () =>
import('./product-reviews/product-reviews.component')
.then(m => m.ProductReviewsComponent),
},
],
},
];Navigating to Child Routes
<!-- Absolute path --> <a routerLink="/users/42">User 42</a> <a routerLink="/users/42/edit">Edit User</a> <!-- From inside a child component — relative navigation --> <a routerLink="..">Back to list</a> <!-- parent --> <a routerLink="../43">Next user</a> <!-- sibling param --> <a routerLink="edit">Edit</a> <!-- child of current -->
import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({ standalone: true, template: `...` })
export class UserDetailComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
goToEdit(id: number) {
// Absolute
this.router.navigate(['/users', id, 'edit']);
// Relative to current route
this.router.navigate(['edit'], { relativeTo: this.route });
// Up one level then into a sibling
this.router.navigate(['..', id + 1], { relativeTo: this.route });
}
}Inheriting Parent Parameters
By default, a child component can only see parameters defined in its own route segment. To access parent parameters from a child, enable parameter inheritance.
// Enable param inheritance in router setup
import { provideRouter, withRouterConfig } from '@angular/router';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withRouterConfig({ paramsInheritanceStrategy: 'always' })),
],
});
// Now a child can read parent params via its own ActivatedRoute
// Route: /users/:userId/posts/:postId
// In PostDetailComponent:
const userId = route.snapshot.paramMap.get('userId'); // from parent :userId
const postId = route.snapshot.paramMap.get('postId'); // from own :postIdparamsInheritanceStrategy: 'always', access parent params via route.parent?.snapshot.paramMap.get('userId').Resolvers with Child Routes
Resolvers pre-fetch data before a route activates. They work seamlessly with child routes.
// user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';
export const userResolver: ResolveFn<User> = (route) => {
const id = route.paramMap.get('id')!;
return inject(UserService).getUser(+id);
};
// app.routes.ts — resolve before any child activates
{
path: 'users/:id',
component: UserDetailShellComponent,
resolve: { user: userResolver },
children: [
{ path: '', component: UserOverviewComponent },
{ path: 'posts', component: UserPostsComponent },
],
}// UserDetailShellComponent — read resolved data
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({ standalone: true, template: `...` })
export class UserDetailShellComponent {
private route = inject(ActivatedRoute);
user = this.route.snapshot.data['user']; // pre-fetched before component renders
}Named Outlets in Nested Routes
A component can have both a primary and a named <router-outlet>. The named outlet renders a side panel or modal independently.
// template with named outlet
// <router-outlet></router-outlet> ← primary
// <router-outlet name="detail"></router-outlet> ← named
const routes: Routes = [
{
path: 'products',
component: ProductsPageComponent,
children: [
{ path: '', component: ProductListComponent },
{
path: ':id',
component: ProductPreviewComponent,
outlet: 'detail', // renders in the named outlet
},
],
},
];
// Navigate to populate the named outlet
router.navigate([
'/products',
{ outlets: { detail: [selectedProductId] } },
]);Common Patterns Summary
Pattern | How to implement |
|---|---|
Layout with tabs | Parent = tab shell, children = tab pages |
Master-detail | Parent = list, child = detail panel |
Wizard / stepper | Parent = wizard shell, children = steps |
Protected section | Component-less parent with canActivate guard |
Feature area | loadChildren pointing to feature routes file |
Pre-fetch data | resolve on the parent, read from children via route.parent |
<router-outlet /> to every parent component that defines child routes. It is the single most common cause of child routes appearing to do nothing.Summary: Nest routes using the children array in a route config. The parent component renders the shared layout and a <router-outlet /> where child components appear. Use component-less parents for grouping and shared guards. Combine with lazy loading (loadChildren) for optimal bundle splitting.