Routing
The router gives you declarative, policy-driven navigation with lazy-loading, nested routes, and slots — all through decorators.
Defining routes
Routes are defined with the @Route class decorator on page components:
@Route('/home')
@Component()
export class HomePage {
view() {
return <h1>Welcome</h1>;
}
}
@Route('/products')
@Component()
export class ProductsPage {
view() {
return <h1>Products</h1>;
}
}
Each component is lazy-loaded — the browser only downloads the bundle for the route it needs.
Navigation
Use Router.navigate for programmatic navigation:
@Component()
export class Header {
@Inject(Router) router!: Router;
goToProducts() {
this.router.navigate('/products');
}
view() {
return <button onClick={() => this.goToProducts()}>Products</button>;
}
}
Or use the link behavior on anchor elements for SPA navigation:
<a link href="/home" activeClass="active">Home</a>
<a link href="/products" activeClass="active">Products</a>
<a link href="/about" activeClass="active font-bold">About</a>
The link behavior intercepts clicks, navigates via the router, and applies activeClass when the current route matches.
Route parameters
Use @Param to extract individual parameters, or @Params for all of them:
@Route('/users/:id')
@Component()
export class UserProfile {
@Param('id') userId!: string;
view() {
return <h1>User {this.userId}</h1>;
}
}
@Route('/store/:storeId/product/:productId')
@Component()
export class ProductDetail {
@Params() params!: Record<string, string>;
view() {
return <p>Store {this.params.storeId}, Product {this.params.productId}</p>;
}
}
Query parameters
Use @Query to extract individual query parameters, or @QueryParams for all of them:
@Route('/search')
@Component()
export class SearchPage {
@Query('q') searchQuery!: string;
@Query('page') currentPage!: string;
view() {
return <p>Searching: {this.searchQuery}, page {this.currentPage}</p>;
}
}
URL /search?q=signals&page=2 gives you searchQuery = "signals" and currentPage = "2".
Nested routes
Routes nest naturally. Each RouteView renders the component for its nesting level:
@Route('/dashboard')
@Component()
export class Dashboard {
view() {
return (
<div>
<h1>Dashboard</h1>
<RouteView />
</div>
);
}
}
@Route('/dashboard/settings')
@Component()
export class DashboardSettings {
view() {
return <h2>Settings</h2>;
}
}
The inner RouteView inside Dashboard renders DashboardSettings when the URL is /dashboard/settings.
Route policies
Policies control who can access a route. They are service classes with decorated methods:
@Service
export class AuthPolicy {
@Inject(AuthService) auth!: AuthService;
@Inject(Router) router!: Router;
@Allow()
publicRoute() {
return this.route.meta?.public === true;
}
@Redirect()
redirectToLogin() {
if (!this.auth.isAuthenticated()) {
this.router.navigate('/login');
return true;
}
return false;
}
}
Attach policies to routes through the config:
@Route('/admin', {
policies: [AuthPolicy, AdminRolePolicy]
})
@Component()
export class AdminPage { }
| Decorator | When it returns true |
|---|---|
@Allow |
Navigation is permitted |
@Block |
Navigation is denied |
@Redirect |
Navigation is redirected (must call router.navigate before returning) |
@Skip |
This policy abstains — pass to the next policy |
Policies evaluate in order. The first decisive result wins. If all policies skip, navigation is allowed by default.
Competing routes
Two components can share the same URL. Policies decide which one renders:
@Route('/dashboard', {
policies: [AdminPolicy]
})
@Component()
export class AdminDashboard { }
@Route('/dashboard', {
policies: [UserPolicy]
})
@Component()
export class UserDashboard { }
An admin sees AdminDashboard. A regular user sees UserDashboard. Components are lazy-loaded, so a regular user never downloads the admin bundle.
Route metadata
Attach arbitrary metadata to a route and access it from the component or from policies:
@Route('/admin', {
metadata: { requiresAuth: true, role: 'admin' }
})
@Component()
export class AdminPage {
@RouteMetadata('role') role!: string;
@RouteMetadata() allMeta!: Record<string, any>;
}
Slots
Slots let you render multiple components in parallel for the same route:
@Route('/dashboard', { slot: '@main' })
@Component()
export class DashboardMain { }
@Route('/dashboard', { slot: '@sidebar' })
@Component()
export class DashboardSidebar { }
@Component()
export class AppLayout {
view() {
return (
<div class="layout">
<RouteView routeSlot="@main" />
<aside>
<RouteView routeSlot="@sidebar" />
</aside>
</div>
);
}
}
Each RouteView independently renders the candidate that matches its slot.