Add a Router Layout
This guide teaches you how to create a router layout in Flow, apply it to views automatically and explicitly, and work with nested layouts. A hands-on mini-tutorial at the end will help you put these concepts into practice.
Router Layouts in Flow
In Flow, router layouts are UI components that implement the RouterLayout
interface, which provides two key methods:
-
showRouterLayoutContent(HasElement)
— shows the given view in the router layout. -
removeRouterLayoutContent(HasElement)
— removes the given view in the router layout.
When you navigate to a view, the router first determines which layout to use — if any. If you’re navigating from one view to another inside the same router layout, the existing router layout instance is reused. Otherwise, a new instance is created. The router then calls showRouterLayoutContent()
, passing in the new view instance.
Automatic Layouts
To create an automatic layout, add the @Layout
annotation to a router layout. This layout is automatically applied to all views unless explicitly disabled.
The following example applies MainLayout
to all views:
@Layout
public class MainLayout extends AppLayout { 1
...
}
-
AppLayout
is a built-in router layout. See its documentation page for more details.
Opting Out
Sometimes, you may want to exclude specific views from the automatic layout. For example, displaying a login view within an application layout might not be appropriate.
To prevent a view from using the automatic layout, set the autoLayout
attribute of the @Route
annotation to false
:
@Route(value = "login", autoLayout = false)
public class LoginView extends Main {
...
}
Note
|
@RouteAlias also has the autoLayout attribute. You can disable or enable the automatic layout for a single view depending on the route used to access it.
|
Scoping to a Path
You can restrict an automatic layout to views with routes that start with a specific path.
The following example applies AdminLayout
only to views with routes starting with /admin
:
@Layout("/admin")
public class AdminLayout extends AppLayout {
...
}
If a route matches multiple layouts, the layout with the longest matching path takes precedence. For example, given a main layout scoped to /
(default) and an admin layout scoped to /admin
, the layouts apply as follows:
-
/
→ rendered inside the main layout. -
/customers
→ rendered inside the main layout. -
/admin/users
→ rendered inside the admin layout -
/admin/groups
→ rendered inside the admin layout.
Important
| Defining multiple layouts with the exact same path will result in an exception. |
Explicit Layouts
You can declare a view to use a specific router layout using the layout
attribute of the @Route
annotation:
@Route(layout = MyLayout.class)
public class DefinedLayoutView extends Main {
...
}
Declaring a layout explicitly also disables the automatic layout for the view.
Note
|
@RouteAlias also has the layout attribute. You can render the same view in different layouts depending on the route used to access it.
|
Nested Layouts
Automatic layouts do not apply to other layouts. By default, a router layout is rendered directly in the browser tab. To render a router layout inside another router layout, use the @ParentLayout
annotation:
@ParentLayout(MainLayout.class)
public class NestedLayout extends Div implements RouterLayout {
...
}
Path Prefixes
By default, router layouts do not affect the routes of the views that they are applied to. You can change this with the @RoutePrefix
annotation, which adds a prefix to all its routes.
In the following example, MyView
receives the some
prefix from its router layout, resulting in some/path
being its actual path:
@Route(value = "path", layout = MyLayout.class)
public class MyView extends Main {
...
}
@RoutePrefix("some")
public class MyLayout extends Div implements RouterLayout {
...
}
Opting Out
A view can opt out from a route prefix by setting the absolute
attribute of @Route
to true
.
In the following example, the path of MyView
is path
, ignoring the prefix coming from MyLayout
:
@Route(value = "path", layout = MyLayout.class, absolute = true)
public class MyView extends Main {
...
}
@RoutePrefix("some")
public class MyLayout extends Div implements RouterLayout {
...
}
Note
|
@RouteAlias also has the absolute attribute.
|
Nested router layouts can also opt out from route prefixes.
In the following example, the path of MyView
is in fact nested/path
, as opposed to some/nested/path
:
@Route(value = "path", layout = MyNestedLayout.class)
public class MyView extends Main {
...
}
@RoutePrefix(value = "nested", absolute = true)
@ParentLayout(MyLayout.class)
public class MyNestedLayout extends Div implements RouterLayout {
...
}
@RoutePrefix("some")
public class MyLayout extends Div implements RouterLayout {
...
}
Try It
In this mini-tutorial, you’ll explore router layouts using the Vaadin walking skeleton. You’ll then create a nested layout and experiment with different ways to apply it to views.
Set Up the Project
First, generate a walking skeleton with a Flow UI, open it in your IDE, and run it with hotswap enabled.
Explore the Main Layout
The skeleton already contains a main layout. Instead of implementing one from scratch, you’re going to have a look at it. Open [application package].base.ui.view.MainLayout
in your IDE.
The main layout is based on App Layout:
@Layout
public final class MainLayout extends AppLayout {
public MainLayout() {
setPrimarySection(Section.DRAWER);
addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu());
}
...
}
It has a drawer on the left side with the following elements: an application header, a navigation menu, and a user menu. All the elements are styled using Lumo Utility Classes.
The Header
The header is created by the createHeader()
method. It contains the application’s name and logo:
private Div createHeader() {
// TODO Replace with real application logo and name
var appLogo = VaadinIcon.CUBES.create();
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
var appName = new Span("Walking Skeleton");
appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE);
var header = new Div(appLogo, appName);
header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER);
return header;
}
Now, change the name and the logo. Use an icon from the default icons.
The Navigation Menu
The navigation menu is created by the createSideNav()
method. It includes all views — both Flow and React — that have declared a menu item:
private SideNav createSideNav() {
var nav = new SideNav();
nav.addClassNames(Margin.Horizontal.MEDIUM);
MenuConfiguration.getMenuEntries().forEach(entry -> 1
nav.addItem(createSideNavItem(entry)));
return nav;
}
private SideNavItem createSideNavItem(MenuEntry menuEntry) {
if (menuEntry.icon() != null) { 2
return new SideNavItem(menuEntry.title(), menuEntry.path(),
new Icon(menuEntry.icon())); 3
} else {
return new SideNavItem(menuEntry.title(), menuEntry.path());
}
}
-
MenuConfiguration
gives access to all registered view menu items. -
This navigation menu assumes that all menu items have a title, but only some may have an icon. If you know all your menu items have icons, you can simplify this method.
-
This navigation menu assumes that the
icon
attribute contains the name of an Icon.
The User Menu
The user menu is created by the createUserMenu()
method. It is the only part of the router layout that is a stub:
private Component createUserMenu() {
// TODO Replace with real user information and actions
var avatar = new Avatar("John Smith");
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
avatar.addClassNames(Margin.Right.SMALL);
avatar.setColorIndex(5);
var userMenu = new MenuBar();
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
userMenu.addClassNames(Margin.MEDIUM);
var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add("John Smith");
userMenuItem.getSubMenu().addItem("View Profile");
userMenuItem.getSubMenu().addItem("Manage Settings");
userMenuItem.getSubMenu().addItem("Logout");
return userMenu;
}
The Security guides show you how to add real functionality to the user menu.
Create a Nested Layout
Create a new Java package [application package].tutorial.ui.view
. Inside this package, create a new class called NestedLayout
, like this:
import com.example.application.base.ui.view.MainLayout; 1
import com.vaadin.flow.component.HasElement;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.router.*;
import com.vaadin.flow.theme.lumo.LumoUtility;
@ParentLayout(MainLayout.class) 2
public class NestedLayout extends Div implements RouterLayout {
private final Div content;
public NestedLayout() {
addClassNames(LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Gap.SMALL, LumoUtility.Padding.MEDIUM,
LumoUtility.BoxSizing.BORDER, LumoUtility.Height.FULL);
content = new Div();
content.addClassNames(LumoUtility.Border.ALL, LumoUtility.Background.BASE);
content.setSizeFull();
add(new Div("This is a layout: " + this), content); 3
}
@Override
public void showRouterLayoutContent(HasElement content) { 4
this.content.getElement().appendChild(content.getElement());
}
}
-
Replace with real package.
-
This renders the nested layout inside the main layout.
-
Prints
this
on the screen so that you can see when the layout instance changes. -
This renders views inside the
content
element.
You can’t see what your new layout looks like yet, because you don’t have any views that use it. You’ll fix that next.
Create Example Views
You’ll now create two views that contain links to each other and both use the new nested layout. Inside the [application package].tutorial.ui.view
package, create two new classes; FirstView
and SecondView
:
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouterLink;
@Route(layout = NestedLayout.class)
@Menu
public class FirstView extends Main {
public FirstView() {
add(new H2("First View"),
new RouterLink("Second View", SecondView.class));
}
}
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouterLink;
@Route(layout = NestedLayout.class)
@Menu
public class SecondView extends Main {
public SecondView() {
add(new H2("Second View"),
new RouterLink("First View", FirstView.class));
}
}
Test the Application
Restart the application. Open your browser and navigate to: http://localhost:8080/first
You should see the first view rendered inside the nested layout.
Navigate back and forth between the first view and the second view. You should see that the nested layout instance remains unchanged.
Now click on Task List in the navigation menu, then on FirstView. You should see that the nested layout instance has now changed.
Add a Route Prefix
Add a layout
route prefix to the nested layout:
@ParentLayout(MainLayout.class)
@RoutePrefix("layout")
public class NestedLayout extends Div implements RouterLayout {
...
}
Restart the application. Notice that the paths of first view and second view have now changed to layout/first
and layout/second
, respectively.
Enable Automatic Layout
Up to this point, the nested layout has been explicitly applied to the first view and second view. You’ll now change this so that it is applied automatically, while keeping the application’s behavior unchanged.
Start by removing the reference to NestedLayout
from first view and second view, and add the layout
prefix to each route:
@Route("layout/first")
@Menu
public class FirstView extends Main {
...
}
@Route("layout/second")
@Menu
public class SecondView extends Main {
...
}
Next, change the NestedLayout
to be automatically applied to all paths that start with /layout
. Remove the @RoutePrefix
annotation and add the @Layout
annotation:
@ParentLayout(MainLayout.class)
@Layout("/layout")
public class NestedLayout extends Div implements RouterLayout {
...
}
Restart the application and click around. It should behave the same way as before.
Final Thoughts
You’ve now learned how to:
-
Create a custom router layout.
-
Apply layouts explicitly and automatically.
-
Use path prefixes in router layouts.
Now, try experimenting with route aliases and absolute routes!