TreeNav
Overview
TreeNav is a data-driven horizontal navigation bar with a persistent selection state: top-level entries sit in a row, branch entries open dropdown popups (hover by default), and selecting any leaf keeps the whole ancestor chain highlighted.
STreeNav is the horizontal counterpart of STreeMenu: both consume the same tree-shaped items data (TreeNavOptionData) and derive highlights from the selected value, but TreeNav renders branches as DropdownMenu popups instead of collapsible sections. Logic and accessibility semantics live in the Aria TreeNavCompact; styles are injected through an scv() recipe.
Unlike Menubar (a transient “which menu is open” command-menu model), TreeNav models selection: opening a popup never marks anything active — only selection does. Selected leaves carry
data-selected, and ancestors of the selected leaf carrydata-child-selected.
Features
- Persistent selection — bind
modelValue(v-model) or seed withdefaultValue; selection survives closing popups and page interactions. - Highlight derivation — the selected leaf renders
data-selected="true", and every ancestor in its path rendersdata-child-selected; derivation reuses the shared tree-path helper, so highlighting works at any depth. - Hover-first popups — branch popups open on hover by default (
trigger="click"to override), tuned viadelayDuration/skipDelayDuration. - Familiar data shape — pass one
itemsarray (TreeNavOptionData:value/label/icon/children/href/to/disabled…), aligned with TreeMenu’s option model; item customization beyond these fields (badges, tags, actions…) is provided via slots instead of extra data fields. - Link top-level items — items with
href/torender as links that also update the selection on click. - Overflow collapsing — with
collapsible, trailing top-level items merge into a trailing “more” popup so the bar always fits its container; customize viamoreLabel/moreIcon/moreProps/ themore-triggerslot. - Navigation semantics — root renders as
<nav>(asoverridable); popup trigger semantics (aria-haspopup/aria-expanded, Escape handling) are inherited from the DropdownMenu layer. - Keyboard navigation — the top level is a single roving tab stop: ←/→ roam all entries alike (skipping disabled, no wrap), Home/End jump to the first/last entry; branch popups open through the explicit keys — Enter/Space (native button semantics) or ArrowDown — and once a popup is open its keys are owned by the Menu machinery.
- Per-item and whole-bar disabling —
item.disabledmakes a single entry inert; the bar-leveldisableddisables everything. - Hidden items —
item.hiddendrops the entry and its subtree from the bar and its popup; a branch whose children are all hidden renders as a plain leaf. - Six sizes & two variants —
size(xs…2xl) plusvariant="default"(subtle card surface) orvariant="nav"(bare bar). - Slot passthrough —
item/item-leading/item-trailing/item-trigger-icon/item-link-iconforward into both the top level and popups. - Aria composition — import from
@vean/aria/tree-navfor unstyled data-driven usage; popup options reuseMenuOptionsCompact.
Usage
Demos
Selection
Selected: email — opening a popup never marks a branch active; only selection does.
Disabled
Overflow Collapsing
API
Aria Layer
6TreeNav
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | class of tree-nav root. |
size | ThemeSize | — | Visual size of the component. |
ui | Partial<TreeNavUi> | — | Per-slot class overrides for the component. |
collapsible | boolean | false | Whether top-level overflow items collapse into a trailing "more" branch
popup so the bar always fits inside its container. |
modelValue | string | — | The value of the currently selected item. Can be used as `v-model`. |
defaultValue | string | — | The value of the item that should be selected when initially rendered. |
dir | Direction | — | The reading direction of the component when applicable. |
trigger | DropdownMenuTriggerType | 'hover' | How branch popups are opened.
- `click`: The popup will be opened when the trigger is clicked.
- `hover`: The popup will be opened when the trigger is hovered. |
delayDuration | number | 150 | The duration from when the pointer enters a branch trigger until the
popup gets opened in hover mode. |
skipDelayDuration | number | 300 | How much time a user has to enter another branch trigger without
incurring a delay again. |
placement | Placement | 'bottom-start' | The preferred placement of branch popups relative to their trigger. |
showArrow | boolean | false | Whether branch popups show an arrow. |
disabled | boolean | — | Whether the whole navigation bar is disabled. |
linkProps | LinkExtraProps | — | Properties forwarded to the link element of link items. |
itemProps | MenuItemProps | — | Properties forwarded to the item element of popup items. |
groupLabelProps | MenuGroupLabelProps | — | Properties forwarded to the group label element of popup group labels. |
shortcutProps | MenuShortcutProps | — | Properties forwarded to the shortcut element of popup items. |
separatorProps | MenuSeparatorProps | — | Properties forwarded to the separator element of popup separators. |
subTriggerProps | MenuSubTriggerProps | — | Properties forwarded to the sub trigger element of popup branches. |
subContentProps | MenuSubContentProps | — | Properties forwarded to the sub content element of popup branches. |
portalProps | MenuPortalProps | — | Properties forwarded to the portal element of popups. |
popupProps | MenuPopupProps | — | Properties forwarded to the popup element of popups. |
triggerProps | DropdownMenuTriggerProps | — | Properties forwarded to the trigger element of branch popups. |
arrowProps | MenuArrowProps | — | Properties forwarded to the arrow element of popups. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
items | TreeNavOptionData<TreeNavBaseOptionData>[] | — | Top-level items rendered as visible entries. |
moreItems | TreeNavOptionData<TreeNavBaseOptionData>[] | — | Items collapsed into the trailing "more" popup. |
moreLabel | string | 'More' | Label of the trailing "more" trigger. |
moreIcon | IconValue | 'lucide:ellipsis' | Icon of the trailing "more" trigger. |
moreProps | ButtonProps | — | Properties forwarded to the trailing "more" trigger button. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string | Emitted when the selected value changes. |
select | TreeNavOptionData<TreeNavBaseOptionData>, event: Event | Emitted when an item is selected. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | { item: MenuOptionData<DefinedValue>; isTrigger?: boolean; } | Custom content for the item slot. |
item-leading | { item: MenuOptionData<DefinedValue>; } | Custom content for the item leading slot. |
item-trailing | { item: MenuOptionData<DefinedValue>; } | Custom content for the item trailing slot. |
item-trigger-icon | { item: MenuOptionData<DefinedValue>; } | Custom content for the item trigger icon slot. |
item-link-icon | { item: MenuOptionData<DefinedValue>; } | Custom content for the item link icon slot. |
more-trigger | TreeNavMoreEntry | Custom content for the trailing "more" trigger when present. |
TreeNavCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
collapsible | boolean | false | Whether top-level overflow items collapse into a trailing "more" branch
popup so the bar always fits inside its container. |
modelValue | string | — | The value of the currently selected item. Can be used as `v-model`. |
defaultValue | string | — | The value of the item that should be selected when initially rendered. |
dir | Direction | — | The reading direction of the component when applicable. |
trigger | DropdownMenuTriggerType | 'hover' | How branch popups are opened.
- `click`: The popup will be opened when the trigger is clicked.
- `hover`: The popup will be opened when the trigger is hovered. |
delayDuration | number | 150 | The duration from when the pointer enters a branch trigger until the
popup gets opened in hover mode. |
skipDelayDuration | number | 300 | How much time a user has to enter another branch trigger without
incurring a delay again. |
placement | Placement | 'bottom-start' | The preferred placement of branch popups relative to their trigger. |
showArrow | boolean | false | Whether branch popups show an arrow. |
disabled | boolean | — | Whether the whole navigation bar is disabled. |
linkProps | LinkExtraProps | — | Properties forwarded to the link element of link items. |
itemProps | MenuItemProps | — | Properties forwarded to the item element of popup items. |
groupLabelProps | MenuGroupLabelProps | — | Properties forwarded to the group label element of popup group labels. |
shortcutProps | MenuShortcutProps | — | Properties forwarded to the shortcut element of popup items. |
separatorProps | MenuSeparatorProps | — | Properties forwarded to the separator element of popup separators. |
subTriggerProps | MenuSubTriggerProps | — | Properties forwarded to the sub trigger element of popup branches. |
subContentProps | MenuSubContentProps | — | Properties forwarded to the sub content element of popup branches. |
portalProps | MenuPortalProps | — | Properties forwarded to the portal element of popups. |
popupProps | MenuPopupProps | — | Properties forwarded to the popup element of popups. |
triggerProps | DropdownMenuTriggerProps | — | Properties forwarded to the trigger element of branch popups. |
arrowProps | MenuArrowProps | — | Properties forwarded to the arrow element of popups. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
items | TreeNavOptionData<TreeNavBaseOptionData>[] | — | Top-level items rendered as visible entries. |
moreItems | TreeNavOptionData<TreeNavBaseOptionData>[] | — | Items collapsed into the trailing "more" popup. |
moreLabel | string | 'More' | Label of the trailing "more" trigger. |
moreIcon | IconValue | 'lucide:ellipsis' | Icon of the trailing "more" trigger. |
moreProps | ButtonProps | — | Properties forwarded to the trailing "more" trigger button. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string | Emitted when the selected value changes. |
select | TreeNavOptionData<TreeNavBaseOptionData>, event: Event | Emitted when an item is selected. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | { item: MenuOptionData<DefinedValue>; isTrigger?: boolean; } | Custom content for the item slot. |
item-leading | { item: MenuOptionData<DefinedValue>; } | Custom content for the item leading slot. |
item-trailing | { item: MenuOptionData<DefinedValue>; } | Custom content for the item trailing slot. |
item-trigger-icon | { item: MenuOptionData<DefinedValue>; } | Custom content for the item trigger icon slot. |
item-link-icon | { item: MenuOptionData<DefinedValue>; } | Custom content for the item link icon slot. |
more-trigger | TreeNavMoreEntry | Custom content for the trailing "more" trigger when present. |
TreeNavOptionCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
item | TreeNavOptionData<T extends TreeNavBaseOptionData = TreeNavBaseOptionData> | — | Current item data. |
childSelected | boolean | — | Whether the selected leaf lives inside this branch subtree. |
TreeNavOptionsCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | TreeNavOptionData<TreeNavBaseOptionData>[] | — | Top-level items rendered as visible entries. |
moreItems | TreeNavOptionData<TreeNavBaseOptionData>[] | — | Items collapsed into the trailing "more" popup. |
moreLabel | string | 'More' | Label of the trailing "more" trigger. |
moreIcon | IconValue | 'lucide:ellipsis' | Icon of the trailing "more" trigger. |
moreProps | ButtonProps | — | Properties forwarded to the trailing "more" trigger button. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | { item: MenuOptionData<DefinedValue>; isTrigger?: boolean; } | Custom content for the item slot. |
item-leading | { item: MenuOptionData<DefinedValue>; } | Custom content for the item leading slot. |
item-trailing | { item: MenuOptionData<DefinedValue>; } | Custom content for the item trailing slot. |
item-trigger-icon | { item: MenuOptionData<DefinedValue>; } | Custom content for the item trigger icon slot. |
item-link-icon | { item: MenuOptionData<DefinedValue>; } | Custom content for the item link icon slot. |
more-trigger | TreeNavMoreEntry | Custom content for the trailing "more" trigger when present. |
TreeNavOverflow
TreeNavRoot
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | The value of the currently selected item. Can be used as `v-model`. |
defaultValue | string | — | The value of the item that should be selected when initially rendered. |
dir | Direction | — | The reading direction of the component when applicable. |
trigger | DropdownMenuTriggerType | 'hover' | How branch popups are opened.
- `click`: The popup will be opened when the trigger is clicked.
- `hover`: The popup will be opened when the trigger is hovered. |
delayDuration | number | 150 | The duration from when the pointer enters a branch trigger until the
popup gets opened in hover mode. |
skipDelayDuration | number | 300 | How much time a user has to enter another branch trigger without
incurring a delay again. |
placement | Placement | 'bottom-start' | The preferred placement of branch popups relative to their trigger. |
showArrow | boolean | false | Whether branch popups show an arrow. |
disabled | boolean | — | Whether the whole navigation bar is disabled. |
linkProps | LinkExtraProps | — | Properties forwarded to the link element of link items. |
itemProps | MenuItemProps | — | Properties forwarded to the item element of popup items. |
groupLabelProps | MenuGroupLabelProps | — | Properties forwarded to the group label element of popup group labels. |
shortcutProps | MenuShortcutProps | — | Properties forwarded to the shortcut element of popup items. |
separatorProps | MenuSeparatorProps | — | Properties forwarded to the separator element of popup separators. |
subTriggerProps | MenuSubTriggerProps | — | Properties forwarded to the sub trigger element of popup branches. |
subContentProps | MenuSubContentProps | — | Properties forwarded to the sub content element of popup branches. |
portalProps | MenuPortalProps | — | Properties forwarded to the portal element of popups. |
popupProps | MenuPopupProps | — | Properties forwarded to the popup element of popups. |
triggerProps | DropdownMenuTriggerProps | — | Properties forwarded to the trigger element of branch popups. |
arrowProps | MenuArrowProps | — | Properties forwarded to the arrow element of popups. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string | Emitted when the selected value changes. |
select | TreeNavOptionData<TreeNavBaseOptionData>, event: Event | Emitted when an item is selected. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | — | Default content rendered inside the nav root. |
Notes
When to use which navigation component
| Component | State model | Layout | Use for |
|---|---|---|---|
SMenubar | open menu (modelValue = which menu is open) | horizontal | application command menus (File/Edit) |
SNavMenu | highlighted item (modelValue) | horizontal | site-level nav with flyout panels |
STreeMenu | persistent selection | vertical | sidebar navigation |
STreeNav | persistent selection | horizontal | top navigation bars |
Runtime considerations
- Open ≠ active — popup open state is transient UI state managed internally by the DropdownMenu layer; only leaf selection updates
data-selected. Do not useSMenubar’smodelValuesemantics here. - Collapsed highlight consistency — when a selected item collapses into the “more” popup, its visible ancestor still shows
data-child-selectedbecause derivation runs on the fullitemslist. - Collapsible measurement — like Menubar’s overflow collapsing, measurement runs against real layout after mount; give the parent a constrained width (e.g.
max-w-*/ fixed width) for best results. The first frame may briefly render all items before collapsing. - Controlled mode — with
modelValue, internal writes only emitupdate:modelValue; uncontrolled usage seeds the initial state withdefaultValue. - Keyboard model — while the bar is closed, ←/→ (and Home/End) roam the top level through a roving tabindex; branch triggers are roam targets like any other entry, so arrows never open popups. Opening stays on Enter/Space or ArrowDown (focus then moves to the first menu item); once a popup is open, all keys belong to the Menu (↑/↓ inside, Escape to close and return focus to the trigger), and a hover popup closes itself when focus leaves the trigger.
FAQ
Which keyboard shortcuts does the nav bar support?
←/→ roam the top-level entries (Home/End jump to the ends) — branch triggers included, so the arrows only move focus and never open popups. Popups open via Enter/Space or ArrowDown on a branch trigger (focus lands on the first menu item). Direction keys never change the selection — selection only happens on explicit activation (click or Enter/Space on a leaf).
How do I make a top-level entry navigate instead of opening a dropdown?
Set href or to on the item — link entries render as <a> elements and update the selection on click.
How do I control which entry stays highlighted?
Bind v-model. The selected value is compared against the whole items tree; matched leaves get data-selected and their ancestors get data-child-selected.
Why doesn’t hovering a branch mark it active?
Opening a popup is transient state, not selection — select a leaf inside instead. This mirrors STreeMenu, where container nodes never carry the active state.
What happens when my nav bar overflows?
Pass collapsible. Trailing items collapse into a “more” popup (default label More) whenever they exceed the container width, recomputed automatically on resize.