Vaadin layouts and navigation: AppLayout, SideNav, and routes
Structure a Vaadin Flow application with AppLayout and SideNav, arrange components with layouts, and navigate between views using routes and URL parameters.
From one view to an application
A single view is a good start, but a real application has many screens: a dashboard, lists, detail pages, settings. Users expect a consistent header and menu, meaningful URLs they can bookmark, and a working browser back button. Vaadin Flow's router and layout components handle all of this.
The examples target Vaadin 24 or later with Spring Boot.
Basic layouts
Before building the application shell, it helps to know the three layouts you will use most:
var title = new H2("New order");
var customer = new TextField("Customer");
var product = new ComboBox<String>("Product");
var quantity = new IntegerField("Quantity");
var save = new Button("Save");
var cancel = new Button("Cancel");
var buttons = new HorizontalLayout(save, cancel);
var form = new FormLayout(customer, product, quantity);
form.setResponsiveSteps(
new FormLayout.ResponsiveStep("0", 1),
new FormLayout.ResponsiveStep("600px", 2));
var page = new VerticalLayout(title, form, buttons);
VerticalLayoutstacks components top to bottom.HorizontalLayoutplaces components side by side.FormLayoutarranges fields into responsive columns. Here it shows one column on narrow screens and two columns from 600 pixels wide.
Control spacing and alignment with methods such as setPadding, setSpacing, setAlignItems, and setJustifyContentMode, and give components room to grow with setFlexGrow or setWidthFull().
The application shell with AppLayout
AppLayout provides the classic structure of a business application: a top navigation bar, a collapsible side drawer, and a content area.
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.theme.lumo.LumoUtility;
public class MainLayout extends AppLayout {
public MainLayout() {
var title = new H1("Shop Admin");
title.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
addToNavbar(new DrawerToggle(), title);
addToDrawer(createNavigation());
setPrimarySection(Section.DRAWER);
}
private SideNav createNavigation() {
var nav = new SideNav();
nav.addItem(new SideNavItem("Dashboard", DashboardView.class, VaadinIcon.DASHBOARD.create()));
nav.addItem(new SideNavItem("Orders", OrdersView.class, VaadinIcon.CART.create()));
nav.addItem(new SideNavItem("Customers", CustomersView.class, VaadinIcon.USERS.create()));
return nav;
}
}
SideNav highlights the item for the current route automatically, and on small screens the drawer collapses behind the toggle button.
Attaching views to the layout
Tell each view which layout it lives in with the layout attribute of @Route:
@Route(value = "", layout = MainLayout.class)
@PageTitle("Dashboard")
public class DashboardView extends VerticalLayout {
public DashboardView() {
add(new H2("Today's overview"));
}
}
@Route(value = "orders", layout = MainLayout.class)
@PageTitle("Orders")
public class OrdersView extends VerticalLayout {
// ...
}
When the user navigates, only the content area changes. The navbar and drawer stay in place, keeping their state.
If almost every view uses the same layout, annotate the layout class with @Layout (available in recent Vaadin versions) and it will be applied automatically to all routes, so you can write @Route("orders") without the layout attribute.
Navigating between views
There are two ways to navigate.
Links render as real <a> elements, so users can open them in a new tab:
var link = new RouterLink("View all orders", OrdersView.class);
Programmatic navigation is used after an action, for example after saving a form:
save.addClickListener(event -> {
orderService.save(order);
UI.getCurrent().navigate(OrdersView.class);
});
Route parameters
Detail pages need an identifier in the URL, such as /orders/42. Declare a route template and read the parameter in beforeEnter:
@Route(value = "orders/:orderId(\\d+)", layout = MainLayout.class)
@PageTitle("Order details")
public class OrderDetailView extends VerticalLayout implements BeforeEnterObserver {
private final OrderService orderService;
private final H2 heading = new H2();
public OrderDetailView(OrderService orderService) {
this.orderService = orderService;
add(heading);
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
long orderId = event.getRouteParameters().getLong("orderId").orElseThrow();
orderService.findById(orderId).ifPresentOrElse(
order -> heading.setText("Order #" + order.getId() + " for " + order.getCustomer()),
() -> event.forwardTo(OrdersView.class));
}
}
The (\\d+) part is a regular expression, so /orders/abc does not match this route at all. If an order does not exist, the user is forwarded back to the list.
Navigate to a detail page from code with RouteParameters:
UI.getCurrent().navigate(OrderDetailView.class, new RouteParameters("orderId", String.valueOf(order.getId())));
Query parameters
Filters and search terms belong in query parameters, so a filtered list can be bookmarked or shared:
@Override
public void beforeEnter(BeforeEnterEvent event) {
List<String> values = event.getLocation().getQueryParameters().getParameters()
.getOrDefault("status", List.of());
values.stream().findFirst().ifPresent(statusFilter::setValue);
}
Update the URL when the filter changes without reloading the view:
statusFilter.addValueChangeListener(e -> {
var query = QueryParameters.of("status", e.getValue());
UI.getCurrent().navigate(OrdersView.class, query);
});
Dynamic page titles
@PageTitle sets a fixed title. For titles that depend on data, implement HasDynamicTitle:
public class OrderDetailView extends VerticalLayout implements BeforeEnterObserver, HasDynamicTitle {
private String title = "Order";
@Override
public String getPageTitle() {
return title;
}
}
Set title inside beforeEnter, and the browser tab shows something like "Order #42".
Navigation lifecycle hooks
Vaadin offers observers for each step of navigation:
| Interface | Called when | Typical use |
|---|---|---|
BeforeEnterObserver |
before a view is shown | load data, check permissions, redirect |
AfterNavigationObserver |
after navigation completes | update breadcrumbs or selection |
BeforeLeaveObserver |
before leaving a view | warn about unsaved changes |
A BeforeLeaveObserver can postpone navigation and ask for confirmation:
@Override
public void beforeLeave(BeforeLeaveEvent event) {
if (binder.hasChanges()) {
var action = event.postpone();
var dialog = new ConfirmDialog("Unsaved changes", "Discard your changes?", "Discard", e -> action.proceed());
dialog.setCancelable(true);
dialog.open();
}
}
Exercise
Build a MainLayout with three menu items, an OrdersView with a status filter stored in the status query parameter, and an OrderDetailView at orders/:orderId. Show "Order #ID" in the browser tab, and warn the user before leaving the detail page if they edited a note field.
