-
Notifications
You must be signed in to change notification settings - Fork 0
How to Create an Accessible Application Shell with Vaadin 8
This post walks through building an accessible application shell in Vaadin 8 using the Valo theme menu pattern and adding ARIA attributes with a small helper extension. It is based on the AppLayout in this project and uses the same ideas: a responsive "Valo menu" layout, keyboard shortcuts, screen‑reader support, and integration with a simple access‑control layer.
The examples assume:
- Vaadin 8 + Valo theme
- Navigator-based navigation
- Custom
AttributeExtension/HasAttributeshelper for ARIA - View-level access annotations (
@AllPermitted,@RolesPermitted)
A typical Vaadin 8 application shell wraps a Navigator and the main menu into a single composite:
public class AppLayout extends Composite {
private final HorizontalLayout layout = new HorizontalLayout();
private final VerticalLayout content = new VerticalLayout();
private final CssLayout menuLayout = new CssLayout();
public AppLayout(UI ui, AccessControl accessControl) {
Navigator nav = new Navigator(ui, content);
nav.setErrorView(ErrorView.class);
ui.setNavigator(nav);
layout.setSizeFull();
layout.addComponents(menuLayout, content);
layout.setExpandRatio(content, 1f);
setCompositionRoot(layout);
}
}This is a standard Vaadin 8 pattern:
-
UIowns a singleNavigator. - The shell hosts the navigation menu and the view container.
- Views are registered on the navigator and switched based on the URI fragment.
The layout itself uses Valo theme conventions:
-
ValoTheme.UI_WITH_MENUon the UI -
ValoTheme.MENU_ROOT,ValoTheme.MENU_PART,ValoTheme.MENU_ITEMS,ValoTheme.MENU_ITEM,ValoTheme.MENU_SELECTEDfor the menu
Valo gives you the visual part, but you still need to provide ARIA roles and labels for assistive technologies. This project does that with a small JavaScript extension.
AttributeExtension is an AbstractJavaScriptExtension attached to any AbstractComponent. It lets you set custom attributes on the component's root DOM element:
AttributeExtension.of(menuLayout)
.setAttribute(AriaAttributes.ROLE, AriaRoles.REGION);The nested helper types encapsulate common ARIA tokens:
-
AriaAttributes.ROLE,AriaAttributes.LABEL,AriaAttributes.KEYSHORTCUTS, … -
AriaRoles.NAVIGATION,AriaRoles.MAIN,AriaRoles.LINK, …
HasAttributes<T> is a small mix‑in interface that components can implement to get convenience methods like setRole and setAttribute that internally use the extension. In the shell, both the navigation container and the menu buttons implement this.
The app shell declares explicit landmark regions so screen readers can jump quickly between menu and main content:
// Menu as a region
AttributeExtension.of(menuLayout)
.setAttribute(AriaAttributes.ROLE, AriaRoles.REGION);
// Main content
AttributeExtension.of(content)
.setAttribute(AriaAttributes.ROLE, AriaRoles.MAIN);With this in place, assistive tools can announce the menu and the main content area correctly, rather than a generic layout of divs.
The navigation container is another composite that implements HasAttributes and adds its own ARIA metadata:
static class Navigation extends Composite implements HasAttributes<Navigation> {
private final CssLayout items = new CssLayout();
public Navigation() {
setCompositionRoot(items);
items.addStyleName(ValoTheme.MENU_ITEMS);
setRole(AriaRoles.NAVIGATION);
setAttribute(AriaAttributes.KEYSHORTCUTS, "Alt+Shift+N");
}
}Key points:
-
role="navigation"marks this part as the global navigation landmark. -
aria-keyshortcuts="Alt+Shift+N"documents the keyboard shortcut that focuses the menu.
Each entry in the Valo menu is a custom MenuButton that extends Button and implements HasAttributes<MenuButton> to get ARIA helpers.
A single menu item is created like this:
public class MenuButton extends Button implements HasAttributes<MenuButton> {
private final String path;
private final String caption;
public MenuButton(String caption, String path, Resource icon, UI ui) {
super(caption);
this.path = path;
this.caption = caption;
setId(path);
setData(path);
setPrimaryStyleName(ValoTheme.MENU_ITEM);
if (path.isEmpty()) {
addStyleName(ValoTheme.MENU_SELECTED);
}
setIcon(icon);
setRole(AriaRoles.LINK);
addClickListener(e -> ui.getNavigator().navigateTo(path));
}
}This follows Valo conventions (style names) and adds role="link" so a screen reader knows that clicking the button causes navigation.
When a view changes, the shell updates the selected menu item and its accessible label:
public void setSelected(boolean selected) {
if (selected) {
addStyleName(ValoTheme.MENU_SELECTED);
setAriaLabel(String.format("%s %s", caption,
getTranslation(I18n.CURRENT_PAGE)));
} else {
removeStyleName(ValoTheme.MENU_SELECTED);
setAriaLabel(caption);
}
}On the navigation container, setSelected(String path) walks all buttons and calls setSelected for the one matching the current path. The result:
- The active view is visually highlighted.
- The corresponding menu button is announced as the current page.
The shell registers a global shortcut listener on the UI to focus the first menu item:
class NavigationFocusListener extends ShortcutListener {
public NavigationFocusListener() {
super("Navigation", KeyCode.N,
new int[] { ModifierKey.ALT, ModifierKey.SHIFT });
}
@Override
public void handleAction(Object sender, Object target) {
toggleButton.click();
firstItem.focus();
}
}Combined with the aria-keyshortcuts attribute on the navigation container, users can both discover and trigger the shortcut.
When the responsive menu is toggled (for example on mobile), the shell uses assistive notifications to announce the state:
toggleButton = new Button(getTranslation(I18n.App.MENU), e -> {
if (menuLayout.getStyleName().contains(ValoTheme.MENU_VISIBLE)) {
menuLayout.removeStyleName(ValoTheme.MENU_VISIBLE);
Notification.show(getTranslation(I18n.App.MENU_CLOSE),
Type.ASSISTIVE_NOTIFICATION);
} else {
menuLayout.addStyleName(ValoTheme.MENU_VISIBLE);
Notification.show(getTranslation(I18n.App.MENU_OPEN),
Type.ASSISTIVE_NOTIFICATION);
}
});The use of Type.ASSISTIVE_NOTIFICATION together with localized messages ensures that assistive technologies get a concise announcement without visual noise.
The application uses a simple, UI‑level access control abstraction:
public interface AccessControl {
boolean signIn(String username, String password);
boolean isUserSignedIn();
boolean isUserInRole(Role role);
String getPrincipalName();
}Views are annotated with one of two annotations to indicate who may access them:
@AllPermitted
public class AboutView extends CustomComponent implements VaadinCreateView { ... }
@RolesPermitted({ Role.ADMIN })
public class AdminView extends CustomComponent implements VaadinCreateView { ... }AppLayout inspects the annotations to decide if a view is accessible:
private boolean hasAccessToView(Class<? extends View> view) {
AllPermitted all = view.getAnnotation(AllPermitted.class);
if (all != null) {
return true;
}
RolesPermitted permitted = view.getAnnotation(RolesPermitted.class);
if (permitted != null) {
for (Role role : permitted.value()) {
if (accessControl.isUserInRole(role)) {
return true;
}
}
return false;
}
throw new IllegalStateException(
"View must have either @AllPermitted or @RolesPermitted");
}The Navigator is configured with a ViewChangeListener that calls this method before navigation. If access is denied, navigation is aborted and a warning is logged.
The UI never registers views directly on the navigator; instead, it calls addView on the shell, which both registers the view and creates a corresponding menu button only if access is allowed:
public void addView(Class<? extends View> view, String viewName,
Resource icon, String path) {
if (!hasAccessToView(view)) {
return;
}
MenuButton menuItem = new MenuButton(viewName, path, icon, ui);
ui.getNavigator().addView(path, view);
menuItems.addMenuButton(menuItem);
}This keeps the menu in sync with what the current user is allowed to see and avoids dangling, inaccessible links in the UI.
The main UI bootstraps either the login view or the application shell depending on the authentication state:
@Theme("vaadincreate")
public class VaadinCreateUI extends UI {
@Override
protected void init(VaadinRequest request) {
if (!getAccessControl().isUserSignedIn()) {
showLoginView();
} else {
showAppLayout();
}
}
protected void showAppLayout() {
AppLayout appLayout = new AppLayout(this, getAccessControl());
setContent(appLayout);
appLayout.addView(AboutView.class, getTranslation(AboutView.VIEW_NAME),
VaadinIcons.INFO, AboutView.VIEW_NAME);
appLayout.addView(BooksView.class, getTranslation(BooksView.VIEW_NAME),
VaadinIcons.TABLE, BooksView.VIEW_NAME);
appLayout.addView(StatsView.class, getTranslation(StatsView.VIEW_NAME),
VaadinIcons.CHART, StatsView.VIEW_NAME);
appLayout.addView(AdminView.class, getTranslation(AdminView.VIEW_NAME),
VaadinIcons.USERS, AdminView.VIEW_NAME);
}
}This keeps the overall flow simple:
- User signs in via a regular Vaadin 8 login view.
-
AccessControlis stored in the session. -
AppLayoutis created with a reference toAccessControl. - Views are registered via
addView, which applies both access checks and menu wiring.
The shell uses an I18n key class and a shared HasI18N mix‑in to keep all user‑visible strings translatable:
- Menu labels (
I18n.App.MENU,I18n.App.MENU_OPEN,I18n.App.MENU_CLOSE) - Logout caption and tooltip
- Assistive messages (e.g.
I18n.CURRENT_PAGE,I18n.OPENED)
Views implementing VaadinCreateView can call openingView(key) to:
- Set the page title using a translation key.
- Emit an assistive notification announcing that the view was opened.
This combination of ARIA roles, keyboard shortcuts, Valo styling, and localized assistive messages gives a Vaadin 8 application shell that behaves much closer to a native, accessible web application than the default Valo menu alone.