Navigation, Child Views & Code Splitting

Learn how to set up view routes, parent layouts, and code splitting with Hilla Router.

Up to now, you have had only one view: the list view. In this chapter, you add a second, dashboard view, to the application. Besides this second view, you also add a parent layout that contains navigation links and a header.

A screenshot of the application with the header and sidebar highlighted.

This chapter covers:

  • using Hilla Router to navigate between views,

  • defining a parent layout,

  • updating the page title on navigation,

  • optimizing application performance with code splitting.

Creating a Parent Application Layout

Hilla comes with two parent classes for views: View and Layout. Layout uses Shadow DOM. This allows it to support child components through the <slot> element.

Shadow DOM encapsulates CSS, which means that global CSS doesn’t apply to content inside layouts. Theme CSS defined in the themes/fusioncrmtutorial/styles.css file is available in views that extend Layout.

Create a new file, frontend/main-layout.ts:

import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import '@vaadin/app-layout';
import '@vaadin/app-layout/vaadin-drawer-toggle.js';
import { Layout } from 'Frontend/views/view';
import { views } from 'Frontend/routes';

export class MainLayout extends Layout {
 connectedCallback() {
   this.classList.add('flex', 'h-full', 'w-full');

MainLayout extends Layout and adds CSS classes for full width and height.

Define the template in the render() method:

render() {
  return html`
    <vaadin-app-layout class="h-full w-full">
      <header slot="navbar" class="w-full flex items-center px-m">
        <h1 class="text-l m-m">Hilla CRM</h1>

      <div slot="drawer">
        <div class="flex flex-col h-full m-l spacing-b-s">
          ${ => html`
            <a href=${view.path}> ${view.title} </a>
      <div class="h-full">
        <slot><!-- The router puts views here --></slot>

The main layout uses Hilla App Layout for a responsive layout with a drawer that can be hidden.

Pass in a <header> component to the application layout using slot="navbar". The header contains a <vaadin-drawer-toggle> to show and hide the drawer.

The drawer content is a div that maps over all views and creates links to them.

Lastly, the main layout contains a <slot></slot> where child views are inserted.

Creating the Dashboard View

Next, create a stub dashboard view. You implement the view in the next chapter but, for now, you need something you can navigate to.

Create a new view, frontend/views/dashboard/dashboard-view.ts, with the following content:

import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { View } from 'Frontend/views/view';

export class DashboardView extends View {
  connectedCallback() {
    this.classList.add('flex', 'flex-col', 'items-center', 'pt-xl');

  render() {
    return html`<h1>Dashboard view</h1>`;

For now, the view only contains an <h1>, which is centered.

Updating Route Definitions

With the main layout and dashboard view created, the next step is to update the route definition file to map them to URL paths.

Update routes.ts with the following content:

import { Route } from '@vaadin/router';
import './views/list/list-view';
import './main-layout';

export type ViewRoute = Route & {
  title?: string;
  children?: ViewRoute[];

export const views: ViewRoute[] = [
    path: '',
    component: 'list-view',
    title: 'Contacts',
    path: 'dashboard',
    component: 'dashboard-view',
    title: 'Dashboard',
    action: async () => {
      await import('./views/dashboard/dashboard-view');

export const routes: ViewRoute[] = [
    path: '',
    component: 'main-layout',
    children: views,

The dashboard view is added to the views array alongside the list view. The routes array is updated to use the main layout and pass the views array as its children.

Code Splitting with Dynamic Imports

You can import views in two ways: statically like list-view and main-layout, or dynamically with import() like dashboard-view.

Dynamic imports help the build tool to split code into smaller chunks that get loaded when you navigate to that view. Using code splitting minimizes the amount of JavaScript the application needs to download when you start it, making it faster. Code splitting helps to keep an application performant, even if it contains a lot of views.

A good rule of thumb when determining whether to use dynamic or static imports is to use static imports for anything that’s always needed for the initial render, and dynamic imports for other views.

In this case, if you were to load main-layout and list-view dynamically, the browser would need to do three round trips to the server: first, to fetch the index page, second, the main layout, and third, the list-view, just to show the root path.

Updating the Page Title on Navigation

The final navigation-related change is to update the page title on navigation. In index.ts, add ViewRoute to the routes import, then add a route-change listener:

window.addEventListener('vaadin-router-location-changed', (e) => {
  const activeRoute = router.location.route as ViewRoute;
  document.title = activeRoute.title ?? 'Hilla CRM';

The listener checks whether the active route has a title property, and uses this to update the document title.

In your browser, verify that you now have a parent application layout and that you can navigate between views.

The list view is now shown inside a parent layout with a header and navigation