Navigation View

FluereNavigationView + FluereNavigationViewItem (plus ...ItemHeader / ...ItemSeparator) map to WinUI 3 / Windows App SDK's Microsoft.UI.Xaml.Controls.NavigationView: a collapsible left pane plus a content area, with groups, multi-level children, a selection indicator and a built-in settings item. The DisplayMode state machine and the key bindings follow the source item by item.

<FluereNavigationView v-model:selected-value="selected" label="Navigation" pane-title="Explorer">
  <FluereNavigationViewItemHeader content="Quick access" />
  <FluereNavigationViewItem value="home" content="Home">
    <template #icon><FluentIconStar24Regular :size="20" /></template>
  </FluereNavigationViewItem>
  <template #content>Content area</template>
</FluereNavigationView>
Selected: Home. The pane adapts to container width (Auto thresholds 1008 / 641).
Basic: groups, icons, selection indicator and settings item

Version and evidence

ItemValue
Spec source tagwinui3/release/2.5.1 (commit ba3a8d5)
Key filessrc/controls/dev/NavigationView/{NavigationView.idl,NavigationView.xaml,NavigationView_themeresources.xaml,NavigationView.cpp,NavigationViewItem.cpp,NavigationViewItemPresenter.cpp,NavigationViewItemHeader.cpp,NavigationViewItemSeparator.cpp} + Generated/{NavigationView,NavigationViewItem}.properties.cpp + SplitView/SplitView_themeresources.xaml + Strings/{en-us,zh-CN}/Resources.resw
Built-in strings4 strings (pane toggle button / settings item) taken verbatim from upstream Resources.resw (see the table in packages/ui/src/navigation-view/locale.ts)

perf2026 is not adopted: upstream ships NavigationBackButton_perf2026.xaml (wired into NavigationView.vcxitems); this library follows the 0.5.1 TitleBar ruling — not read, not referenced, no corresponding branch.

The DisplayMode state machine

paneDisplayMode (mapping to NavigationViewPaneDisplayMode) is configuration; displayMode is the computed result. Do not conflate them.

paneDisplayModeBehaviour
auto (default)Adapts to container width (table below)
leftAlways Expanded (320px pane)
left-compactAlways Compact (48px icon rail)
left-minimalSame as Compact (see difference #2 below)

The auto predicate is copied verbatim from NavigationView.cpp:1452-1465 (default thresholds from NavigationView.idl:176-180):

Container widthUpstream resultThis library
width >= 1008ExpandedExpanded
0 < width < 641MinimalCompact (downgraded, see below)
everything else (incl. 0)CompactCompact
width === 641CompactCompact (the test is strictly less)
NaNCompactCompact
Pane width follows the mode: 320px expanded, 48px compact (icon rail).
Auto switches by container width: >= 1008 expanded, 0 < width < 641 compact (upstream Minimal, downgraded here), otherwise compact.
The three DisplayMode tiers and Auto adaptation

Pane open/close semantics

isPaneOpen is governed by the DisplayMode state machine (item by item from NavigationView.cpp:1494-1512):

  • Entering Expanded: auto-opens only when isPaneVisible is true and the user has not manually closed it;
  • Expanded → Compact: closes unconditionally;
  • Entering Minimal: closes unconditionally (Minimal is downgraded to Compact here, so "anything but Expanded closes");
  • "Manually closed" is a single global flag (upstream m_wasForceClosed, NavigationView.h:466) — there is no per-mode memory; changing pane-display-mode clears it (:4211).

The toggle button shares one code path across all modes (:1662-1674): open → close and set the flag; closed → open and clear it. isPaneToggleButtonVisible only controls visibility, not whether the pane can be toggled (:4403-4407).

A closed pane is not zero-width: it is the 48px icon rail (upstream ListSizeCompact sets PaneContentGrid.Width to CompactPaneLength, NavigationView.xaml:83), showing icons only while the text stays in the accessibility tree.

Alignment rules for the collapsed state (ListSizeCompact)

The collapsed 48px rail follows three rules, driven not by paneDisplayMode but by "compact and closed" (upstream m_isClosedCompact, NavigationViewItem.cpp:253-263):

RuleLevel-A source
Pane title and #paneHeader are hidden togetherListSizeCompact sets Collapsed (NavigationView.xaml:85)
The toggle row keeps no inline padding; inset comes from the button's own Padding = 4,2NavigationViewButtonHolderGridMargin = 0,4 (:232)
The toggle button's icon column matches items (SmallerIconWidth = CompactPaneLength - 8):611-613, NavigationViewItemPresenter.cpp:289

So the hamburger icon in the collapsed state sits in the same column as every item icon (all centred on the 24px midline), and an item's background has symmetric gaps to both pane edges (4px each = the horizontal NavigationViewItemButtonMargin).

Four points previously drifted in this area (the hamburger was squeezed to 7.41 × 20, the title still occupied space, an item background was clipped 3px on the right, and the gear rendered as a solid blob): the first three are covered by the toggle-row / title / divider rules above, while the last was a wrong glyph choice for the settings icon (it must be the linear gear, not an outline-only truncated path). Root causes and fixes are recorded one by one under "the four UI fixes for the collapsed state" in packages/ui/src/navigation-view/navigation-view.vue's file header; the contract tests live in navigation-view.test.ts under "collapsed state (ListSizeCompact) UI regressions".

Keyboard (item by item from the source)

KeyPreconditionEffect
Enterfocus on an iteminvoke (select + expand + emit itemInvoked)
Spacefocus on an item and no Altsame as above; Alt+Space is explicitly not handled (yields to the system window menu, :2752-2761)
↑ / ↓focus on an itemmove focus between visible items; crossing levels (into/out of expanded children, back to the parent) is equivalent to the source's XYFocus plus manual focusing
Home / Endfocus on an itemfocus the first / last focusable item (:2906-2950)
← + Altpane open and light-dismissibleclose the pane (:3136-3145)
Tab—leave the pane (roving tabindex: one Tab stop per group)

Keys the source does not have (negative evidence — do not add them by intuition): → (NavigationView has zero hits for VirtualKey::Right), Esc (closing a Minimal pane belongs to core SplitView.cpp:576-581, not NavigationView), Ctrl+digit.

Holding a key does not repeat invoke: upstream guards with args.KeyStatus().WasKeyDown (:2724); this library uses event.repeat.

Selection and expansion semantics

  • Clicking an item with children both selects it and toggles expansion (OnNavigationViewItemInvoked:1067 does both, not either/or).
  • selects-on-invoked="false": invoke only expands and does not change selection (:1048), useful for group-header-like items; conversely, forcing such an item via selectedValue is rolled back (:2281-2292) — this library falls back to "nothing selected" and warns in dev.
  • isChildSelected: selecting a child marks its entire ancestor chain (UpdateIsChildSelected:2509-2547); here the parent derives it at render time (read-only), so consumers never pass it.
  • Indicator drill-down: starting at the selected item, as long as a node is "not itself selected but has a selected descendant" and its parent is expanded, the indicator is drawn at the lowest visible level (FindLowestLevelContainerToDisplaySelectionIndicator:5562-5593).
  • Closing the pane force-collapses top-level items but remembers their state (HandleExpansionStateMemory, NavigationViewItem.cpp:269-285): reopening the pane restores it. Top-level items only.
Selected: Albums. Selecting a child gives the parent is-child-selected and the indicator drills down.
Groups, hierarchy and ancestor-chain selection

Settings item and slots

SlotMaps to WinUINotes
defaultMenuItemsholds Item / ItemHeader / ItemSeparator
#contentContentPresenterthe content area
#paneHeaderPaneHeaderbelow the toggle button (expanded mode only)
#paneFooterPaneFooterpane bottom, above the settings item
#settingsSettingsItem (FooterMenuItems)replaces the built-in settings item entirely

When the built-in settings item is selected, selectedValue takes a reserved sentinel value (the exported constant FLUERE_NAVIGATION_SETTINGS_VALUE) and itemInvoked reports isSettingsInvoked: true. Consumers must not use that string as a menu item value (a contract test guards this).

Invoking any item emits itemInvoked; the settings item has isSettingsInvoked = true.
Settings item, PaneFooter and events

Differences from WinUI (deliberate, recorded one by one)

  1. Left pane only: PaneDisplayMode's Top is not implemented — top navigation is a different template (TopNavArea / TopNavGrid / overflow Flyout / gamepad BumperNavigation). Consequently ShoulderNavigationEnabled is not implemented either (the source returns false for non-Top, :3165-3170, i.e. the left pane never had gamepad navigation), nor are OverflowLabelMode, AutoSuggestBox and the back/close buttons.
  2. Minimal is downgraded to Compact: upstream Minimal means "pane as an overlay + focus trap" (:4588-4596 sets KeyboardNavigationMode to Cycle); overlay focus management belongs to 0.5.7's useFocusTrap. Here, a width in the Minimal range renders as Compact (a persistent 48px pane) with no overlay.
  3. Children are "data-only subcomponents": the three child components never mount (the parent reads vnode props / slots at render time; see collection.ts), so they cannot emit and their slots do not support scoped arguments.
  4. SelectedItem becomes a value identity: WinUI uses an object reference; the Vue side uses selectedValue (consistent with FluereTabView).
  5. No child flyout: upstream moves children into a ChildrenFlyout when compact and closed (NavigationViewItem.cpp:908-911). Here, compact mode hides the chevron and does not render children, avoiding an arrow that appears to do nothing; expanding the pane shows children inline again.
  6. No "indicator sliding between items" position animation: upstream PlayIndicatorAnimations (NavigationView.cpp:2185-2235) computes the offset between items with Composition, 600ms across three easing segments plus a 200ms centre-point animation. Here only the current item's indicator fades in/out (upstream ShowSelectionIndicator likewise just sets Opacity, NavigationViewItem.cpp:402-408, with no transition), so the static form matches.
  7. The chevron expresses its two end states with a CSS rotation: in the upstream default template RotateExpandCollapseChevron merely calls Begin() on two storyboards that do not exist (NavigationView.xaml has zero Storyboard entries and no Rotate/Angle) ⇒ it is a no-op for the default template; the actual rotation comes from the AnimatedIcon (AnimatedChevronUpDownSmallVisualSource). This library does not ship Lottie and only expresses the two static ends (expanded 180° / collapsed 0°).

Evidence gaps (explicitly recorded)

  • Chevron per-frame keyframes: the LottieGen output gives the total duration 433.33ms and 18 interval markers (AnimatedChevronUpDownSmallVisualSource.cpp:104,782,789-825), but the full path interpolation lives in the JSON source (not in this snapshot). This library only does the two static ends plus a single 50ms transition (override --fui-navigation-view-chevron-duration).
  • Duration of ExpandCollapseRotate{Expanded,Collapsed}Storyboard is unobtainable: NavigationViewItemPresenter.cpp:73-74 looks them up, but they do not exist in the default template, so there is no value to cite.
  • System accent colour: the final value of AccentFillColorDefaultBrush is injected by the OS at runtime; the source tree only has an alias. This library reuses the repository's existing mapping, --colorBrandBackground.
  • High contrast: the HC palette comes from the OS at runtime (themeresources:136-201 are all SystemColor* references); this library implements no HC specifics beyond the forced-colors focus ring already handled by the 0.4.3 primitive.

Token mapping

WinUI resourceFinal colour (light / dark)Token in this library
NavigationViewDefaultPaneBackground ← AcrylicInAppFillColorDefaultBrush#F9F9F9 / #2C2C2C--acrylicInAppFillColorDefault
NavigationViewExpandedPaneBackground ← SolidBackgroundFillColorTransparentfully transparent--colorTransparentBackground (same appearance, different literal — see note 5 in the component header)
NavigationViewContentBackground ← LayerFillColorDefaultBrush#80FFFFFF / #4C3A3A3A--layerFillColorDefault
NavigationViewItemBackground* ← SubtleFillColor{Transparent,Secondary,Tertiary}Brush—--subtleFillColorTransparent / --subtleFillColorSecondary / --subtleFillColorTertiary
NavigationViewItemForeground* ← TextFillColor{Primary,Secondary,Disabled}Brush—--colorNeutralForeground1 / --colorNeutralForeground2 / --colorNeutralForegroundDisabled
NavigationViewSelectionIndicatorForeground ← AccentFillColorDefaultBrushsystem accent--colorBrandBackground
NavigationViewItemSeparatorForeground ← DividerStrokeColorDefaultBrush#0F000000 / #15FFFFFF--dividerStrokeColorDefault
NavigationViewContentGridBorderBrush ← CardStrokeColorDefaultBrush#0F000000 / #19000000--colorNeutralStrokeAlpha (reusing the existing InfoBar / ContentDialog / TabView mapping)
Disabled opacity ← ListViewItemDisabledThemeOpacity0.3--fui-navigation-view-disabled-opacity

Geometry (item by item from NavigationView_themeresources.xaml)

QuantityValueSource
Item min height36:217
Icon box height / icon column16 / max(0, CompactPaneLength - 8) = 40:219, :611-613
Selection indicator3 × 16, radius 2:220-222
Group header height / font size40 / 14 SemiBoldNavigationView.xaml:564, :1081-1086
Separator1px, Margin 0,3,0,4:223, :247
Level indentationDepth() × 31NavigationViewItemBase.h:63
Pane toggle button40 × 36 (MinWidth × MinHeight); collapsed button width = CompactPaneLength(48):205-206, NavigationView.cpp:1823-1829
Corner radiusitem OverlayCornerRadius(8), button ControlCornerRadius(4):447, :283
Pane widthExpanded 320 / Compact 48NavigationView.idl:195,198

Pane open/close motion

The pane width animation comes from the SplitView template (NavigationView's pane is hosted by an internal SplitView):

DirectionResourceValueKeyframe easing
OpenSplitViewPaneAnimationOpenDuration200mscubic-bezier(0, 0.35, 0.15, 1)
CloseSplitViewPaneAnimationCloseDuration100mssame

Both durations map to --durationNormal / --durationFaster, and are set to 0ms under prefers-reduced-motion or motion="none" — turning motion off keeps a sensible static form (the pane still opens and closes, the indicator stays visible, the chevron still points the right way).

API

FluereNavigationView

PropTypeDefaultMaps to WinUI
paneDisplayMode'auto' | 'left' | 'left-compact' | 'left-minimal''auto'PaneDisplayMode
isPaneOpenboolean—IsPaneOpen (v-model:is-pane-open)
defaultIsPaneOpenbooleantruedependency-property default of IsPaneOpen
openPaneLengthnumbertoken 320OpenPaneLength
compactPaneLengthnumbertoken 48CompactPaneLength
compactModeThresholdWidthnumber641CompactModeThresholdWidth
expandedModeThresholdWidthnumber1008ExpandedModeThresholdWidth
isPaneToggleButtonVisiblebooleantrueIsPaneToggleButtonVisible
isPaneVisiblebooleantrueIsPaneVisible
isSettingsVisiblebooleantrueIsSettingsVisible
selectedValuestring | number—SelectedItem (v-model:selected-value)
defaultSelectedValuestring | number——
selectionFollowsFocus'disabled' | 'enabled''disabled'SelectionFollowsFocus
paneTitlestring''PaneTitle
labelstring—AutomationProperties.Name
dir'ltr' | 'rtl''ltr'FlowDirection
motion'auto' | 'none''auto'—
localestring——
Event / slotPayload / notes
update:isPaneOpenboolean
update:selectedValuestring | number | undefined
itemInvoked{ value, content, isSettingsInvoked, source: 'pointer' | 'keyboard' | 'focus' }
selectionChanged{ value, previousValue, isSettingsSelected }
displayModeChanged{ displayMode: 'minimal' | 'compact' | 'expanded' }
#content / #paneHeader / #paneFooter / #settingssee "Settings item and slots" above

FluereNavigationViewItem

PropTypeDefaultMaps to WinUI
valuestring | numberrequired(item identity on the Vue side)
contentstring''Content
selectsOnInvokedbooleantrueSelectsOnInvoked
isExpandedboolean—IsExpanded (v-model:is-expanded)
defaultIsExpandedbooleanfalsedefault of IsExpanded
hasUnrealizedChildrenbooleanfalseHasUnrealizedChildren
isChildSelectedbooleanderivedIsChildSelected (read-only here)
ariaLabelstring—AutomationProperties.Name
disabledbooleanfalseControl.IsEnabled
SlotNotes
defaultitem text / rich content (takes precedence over content)
#iconicon (20px recommended)
#childrenchild items (recursive; may nest #children again)

FluereNavigationViewItemHeader / FluereNavigationViewItemSeparator

ComponentPropsMaps to WinUI
FluereNavigationViewItemHeadercontentNavigationViewItemHeader
FluereNavigationViewItemSeparator—NavigationViewItemSeparator

Both are not focusable and hidden from AT (upstream IsTabStop=False + AccessibilityView=Raw, NavigationView.xaml:505-509, :574-576).

Browser verification

The style contract tests read the SFC / CSS source text (jsdom does not resolve var(), see style-spec §7.1), so "did the pane styles actually apply" must be measured as computed styles in a real browser — especially because this component's row visuals live in the non-scoped navigation-view.css (the same rows are rendered by the parent component and the recursive list component, whose data-v-* ids differ, so one scoped block each would inevitably drift). The measurement results are recorded in the component comments and in docs/todo.md under 0.5.4.

v0.5.0-alpha.1