Skip to content
Glint UI

NavigationMenu

A header nav whose keyboard walks the strip without changing route on every keystroke.

Usage

import { NavigationMenu } from "@glint/components/navigation-menu.slint";
import { Tokens } from "@glint/theme/tokens.slint";
export component AppWindow inherits Window {
width: 560px;
height: 200px;
background: Tokens.color-background;
in-out property <int> route: 0;
VerticalLayout {
padding: 24px;
spacing: 16px;
alignment: start;
NavigationMenu {
accessible-label: "Main";
active <=> root.route;
items: [
{ label: "Overview" },
{ label: "Reports" },
{ label: "Settings" },
];
navigate(index) => { debug("route to", index); }
}
if root.route == 0: Text { text: "Overview"; }
if root.route == 1: Text { text: "Reports"; }
if root.route == 2: Text { text: "Settings"; }
}
}

items is the strip; active is which item is highlighted, bound two-way so the app’s own routing drives it. Clicking a destination fires navigate(index) with active already updated.

The strip is the nav surface and nothing else. It owns no content and switches no screens — that is what tells it from Tabs, which owns the selection its triggers stand for. Reach for Tabs when picking one changes a panel in place, and for this when picking one changes the route.

Two structs describe the model:

TypeFields
NavigationItemlabel, icon, links — destinations this item opens instead of being one
NavigationLinklabel, description (the second line under it), icon

Examples

An item that opens a panel

An item carrying links is a trigger rather than a destination: it draws a chevron, opens a panel of destinations under itself, and fires link-selected(item, link) when one is taken. It never fires navigate, the way a menu row carrying children never fires itself (ADR-0020).

The links are a model rather than a slot, because the panel is repeated once per item and a @children slot is written once (ADR-0027, ADR-0028).

The keyboard’s place is not the route

← and → walk the strip without following what they land on: walking a header nav must not perform a route change per keystroke, so where the keyboard stands (focused-index) is a different thing from which item is active. They stop at both ends rather than wrapping — a route list has a first item and a last one. Enter or Space follows the item the keyboard landed on, or opens its panel; ↓ opens the panel outright.

Both are properties, so an app can see where the keyboard is and put it somewhere.

Tab to the strip and press → a few times: the highlight stays where the route is, and only Enter moves it.

NavigationLink.description is the second line — what the destination is for. It rides along as the row’s accessible description rather than as a text node of its own, so a screen reader hears the link’s name and then its purpose instead of two unrelated strings.

A link with no description draws one line and announces one — the second line is left out of the layout rather than drawn empty.

API Reference

Properties

PropertyTypeDefaultDescription
itemsin [NavigationItem]no defaultItems shown in the nav strip (left-to-right).
activein-out int0Index of the highlighted item; bound two-way so consumers drive routing.
focused-indexin-out introot.activeWhere the keyboard stands in the strip. It starts on the active item and moves with the arrows; consumers rarely set it.
highlighted-indexin-out int0Row the keyboard stands on inside an open panel.

Callbacks

CallbackDescription
navigate(int)Fired when a destination item is taken; active is updated first.
link-selected(int , int)Fired with the trigger a panel hung off and the link taken from it.

NavigationItem and NavigationLink are data types rather than components; their fields are in Usage above.

Accessibility

  • A navigation landmark. The strip carries accessible-role: navigation and publishes its item count. Naming it is the call site’s — set accessible-label.
  • One tab stop, and a delegated announcement. The strip is a single stop, so it delegates the accessible focus to the item the keyboard stands on (focused-index). Without that, a screen reader announces the strip rather than the item the arrows reached.
  • Every item is a named control. Each carries its label, its index and whether it is the active one (accessible-item-selected), and answers the accessible default action.
  • A trigger reports its panel. An item with links declares accessible-expandable and accessible-expanded, and its chevron turns to match. Taking a link closes the panel.
  • An open panel is a list. It carries its row count, and every link is a named list item whose description is announced as its description. ↑ and ↓ move the highlight and wrap at both ends, Home and End jump to its ends, Enter or Space takes the highlighted link, and Esc closes the panel.
  • The focus ring is keyboard-only. It draws around the whole strip when the keyboard put focus there, and not when a click did.