NavigationMenu
A header nav whose keyboard walks the strip without changing route on every keystroke.
import { NavigationMenu } from "@glint/components/navigation-menu.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 660px; height: 220px; background: Tokens.color-background;
in-out property <int> route: 0;
VerticalLayout { padding: 24px; spacing: 16px; alignment: center;
HorizontalLayout { alignment: center;
NavigationMenu { accessible-label: "Main"; active <=> root.route; items: [ { label: "Home", icon: IconSet.House }, { label: "Products", icon: IconSet.Package, links: [ { label: "Analytics", icon: IconSet.ChartLine, description: "Traffic, funnels and retention." }, { label: "Billing", icon: IconSet.CreditCard, description: "Invoices, plans and payment methods." }, { label: "Identity", icon: IconSet.Fingerprint, description: "Sign-in, SSO and access policies." }, ] }, { label: "Pricing", icon: IconSet.Tag }, { label: "Docs", icon: IconSet.BookOpen }, ]; navigate(index) => { root.route = index; } } }
Text { text: "Route " + root.route + " is showing."; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; horizontal-alignment: center; } }}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:
| Type | Fields |
|---|---|
NavigationItem | label, icon, links — destinations this item opens instead of being one |
NavigationLink | label, 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).
import { NavigationMenu } from "@glint/components/navigation-menu.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 660px; height: 260px; background: Tokens.color-background;
property <string> taken: "Open “Resources” and take a link.";
VerticalLayout { padding: 24px; spacing: 16px; alignment: center;
HorizontalLayout { alignment: center;
NavigationMenu { accessible-label: "Site"; items: [ { label: "Home" }, { label: "Resources", links: [ { label: "Documentation", icon: IconSet.BookOpen, description: "Guides and the component reference." }, { label: "Changelog", icon: IconSet.History, description: "What shipped, and when." }, { label: "Support", icon: IconSet.LifeBuoy, description: "Open a ticket or read the FAQ." }, ] }, { label: "Contact" }, ]; link-selected(item, link) => { root.taken = "Link " + link + " under item " + item + "."; } } }
Text { text: root.taken; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; horizontal-alignment: center; } }}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.
import { NavigationMenu } from "@glint/components/navigation-menu.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 660px; height: 240px; background: Tokens.color-background;
in-out property <int> route: 1; in-out property <int> keyboard-at: 1;
VerticalLayout { padding: 24px; spacing: 16px; alignment: center;
HorizontalLayout { alignment: center;
NavigationMenu { accessible-label: "Sections"; active <=> root.route; focused-index <=> root.keyboard-at; items: [ { label: "Inbox" }, { label: "Drafts" }, { label: "Sent" }, { label: "Archive" }, ]; navigate(index) => { root.route = index; } } }
Text { text: "Active route: " + root.route + " · keyboard on: " + root.keyboard-at; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; horizontal-alignment: center; } }}Tab to the strip and press → a few times: the highlight stays where the route is, and only Enter moves it.
A description under the link
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.
import { NavigationMenu } from "@glint/components/navigation-menu.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 660px; height: 280px; background: Tokens.color-background;
VerticalLayout { padding: 24px; alignment: center;
HorizontalLayout { alignment: center;
NavigationMenu { accessible-label: "Platform"; items: [ { label: "Platform", icon: IconSet.Layers, links: [ { label: "Compute", icon: IconSet.Cpu, description: "Machines, autoscaling and regions." }, { label: "Storage", icon: IconSet.Database, description: "Buckets, volumes and retention." }, { label: "Networking", icon: IconSet.Network }, ] }, { label: "Status", icon: IconSet.Activity }, ]; } } }}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
| Property | Type | Default | Description |
|---|---|---|---|
items | in [NavigationItem] | no default | Items shown in the nav strip (left-to-right). |
active | in-out int | 0 | Index of the highlighted item; bound two-way so consumers drive routing. |
focused-index | in-out int | root.active | Where the keyboard stands in the strip. It starts on the active item and moves with the arrows; consumers rarely set it. |
highlighted-index | in-out int | 0 | Row the keyboard stands on inside an open panel. |
Callbacks
| Callback | Description |
|---|---|
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: navigationand publishes its item count. Naming it is the call site’s — setaccessible-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
linksdeclaresaccessible-expandableandaccessible-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
descriptionis 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.