生态产品多个设计器现已发布正式版

查看生态总览
跳转到内容

NavigationMenu 导航菜单 ​

用于站点顶部的多级导航菜单。

用法 ​

从顶部入口展开站点导航

组件结构 ​

加粗的是必需部件。

data-scope="navigation-menu":root · list · item · trigger · trigger-indicator · content · branch-trigger · branch-indicator · branch-content · link · indicator · viewport

示例 ​

竖向排列 ​

在侧栏旁展开子级导航

直达链接 ​

混合下拉入口与普通链接

共享面板 ​

在固定位置切换不同导航内容

面板子级 ​

面板里的条目再展开一层

设计指引 ​

何时使用 ​

  • 门户、营销站或文档站具有多组导航链接。

何时不用 ​

特性 ​

  • 支持横向和竖向排列、延迟展开与键盘导航。
  • 没有子级的入口可直接渲染为链接。
  • 不写结构时照标准结构铺:列表里一项一个 li,带 href 的入口是直达链接,其余是入口按钮(文字取 label)加紧跟其后的面板,面板按 children 铺链接与子级(开关里是文字与展开方向标记);列表末尾是指示条。Vue / React 不写默认插槽与 Web Components 不写部件(或只写 root)时生成的节点出自同一个结构函数;面板内容可由 panel 插槽整块接管(Web Components 用宿主直接子节点 slot="panel:<值>")。入口里的方向标记与共享面板外壳要手写结构时自己摆。
  • 面板里的条目可以再带一层子级:branch-trigger 展开紧跟其后的 branch-content,行尾的 branch-indicator 随之转向下方。同一张面板只展开一枝;子级是面板里的一段,不另起浮层、不动高度、不播展开动画。
  • 每次展开面板都按当前页重新落定子级:当前页链接所在的那一枝展开,其余收起,打开面板就看得到自己在哪。
  • 给了 collection 又没写结构时,入口的 children 铺成面板,面板条目的 children 铺成一枝子级;子级里只放带 href 的链接,不再往下嵌套。value 全树唯一,不合法的嵌套当场报错。
  • viewport 可让所有面板在同一位置切换。
  • 当前链接使用 aria-current="page",并自带一条静态指示线(横排的直达链接贴底边,竖排与面板里的链接贴起始缘);indicator 部件指的是开着的面板,两者各说各的。

组合 ​

  • 窄屏时切换为抽屉或侧栏导航,不压缩顶部入口。

接路由 ​

  • Vue / React 的 link(直达链接与面板里的链接)缺省渲染 <a>,href 由作者写或取自 collection。接客户端路由时给 XhNavigationMenuLink 加 asChild,把路由链接放进去当唯一的子节点:部件属性与按压接线合到它渲出的元素上,跳转交给路由,点击后照常收起面板;子节点不是恰好一个元素时直接报错。
  • 当前页由作者按路由判定后写 current。
  • Web Components 不需要 asChild:link 本来就是作者写的节点,元素只往它身上写属性与监听、不替换它。路由库自己的链接元素,或自行拦截点击的 <a>,直接标 data-xh-part="link" 即可。
vue
<XhNavigationMenuLink :current="route.path === '/docs'" as-child>
  <RouterLink to="/docs">文档</RouterLink>
</XhNavigationMenuLink>
tsx
<XhNavigationMenuLink current={pathname === "/docs"} asChild>
  <Link to="/docs">文档</Link>
</XhNavigationMenuLink>;

最佳实践 ​

  • 使用短标题和简洁说明组织链接。
  • 保留默认展开延时,避免指针经过时连续闪动。

反模式 ​

  • 不要在导航面板中放置表单或一次性命令。
  • 不要在窄屏中强行保留完整横向导航。

API 参考 ​

产物 ​

层值
自定义元素<xh-navigation-menu>
Vue 组件XhNavigationMenuBranchContent XhNavigationMenuBranchIndicator XhNavigationMenuBranchTrigger XhNavigationMenuContent XhNavigationMenuIndicator XhNavigationMenuItem XhNavigationMenuLink XhNavigationMenuList XhNavigationMenuRoot XhNavigationMenuTrigger XhNavigationMenuTriggerIndicator XhNavigationMenuViewport
组合式函数useNavigationMenu
状态机navigationMenuMachine
皮肤@xihan-ui/styles/navigation-menu.css

Props ​

属性类型必填说明
collectionNavigationMenuNode[]入口数据,入口文本与禁用的事实源。提供后 trigger / branch-trigger 部件只需声明 value; 没写结构时按它铺开整套导航:入口的 children 铺成面板,面板条目的 children 铺成一枝子级。 未提供时回到文本与禁用都写在部件上的方式。
valuestring | null当前展开项,提供即受控;null 表示全部收起。
defaultValuestring | null
orientationOrientation方向键轴向,默认 horizontal。
delayDurationnumber悬停 / 聚焦到 trigger 后等待多久才展开,默认 200ms。
skipDelayDurationnumber收起之后的静默窗口,默认 300ms;窗口内再次触及任意 trigger 直接展开。
dirDirection文字方向,默认 ltr。
loopboolean方向键到达末尾是否回绕,默认 true。
disabledboolean整套导航禁用:所有入口都为 aria-disabled,面板不再展开。
translationsPartial<NavigationMenuTranslations>
toneTone语气:brand / neutral / success / warning / danger / info,决定使用哪族颜色。
sizeSize尺寸:sm / md / lg。
onValueChange(details: NavigationMenuValueChangeDetails) => voidvalue 变化回调。

collection 的元素。

字段类型必填说明
valuestring是
labelstring入口文本;默认回退为 value。
disabledboolean入口禁用:方向键跳过它,但它仍可聚焦、仍是导航起点。面板里带子级的条目禁用即展不开。
hrefstring直达目标。提供后该项即为一条链接:顶层没有面板,面板里没有子级。与 children 互斥。
currentboolean指向当前页面的链接:输出 aria-current="page"。
childrenNavigationMenuNode[]下一层条目。顶层入口给了它,缺省结构就按它铺开面板(面板插槽 / renderPanel 仍优先); 面板条目给了它即为一枝可展开的子级,子级里的条目只能是带 href 的链接,不再往下嵌套。

事件 ​

自定义元素将载荷放在 detail;Vue 使用同名 emit。

事件载荷说明
value-changeNavigationMenuValueChangeDetails展开项变化;detail 为 { value: string | null }

React 适配器 props ​

只列各组件自己声明的那些:继承自 ComponentPropsWithRef 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。

React 组件属性类型必填说明
XhNavigationMenuBranchContentvaluestring是
XhNavigationMenuBranchIndicatorvaluestring是
XhNavigationMenuBranchTriggervaluestring是
XhNavigationMenuBranchTriggerdisabledboolean默认交给 connect 查询 collection,写死 false 会覆盖数据中的禁用。
XhNavigationMenuContentvaluestring是
XhNavigationMenuLinkcurrentboolean
XhNavigationMenuRootrenderPanel(node: NavigationMenuNodeMeta) => ReactNode每张面板的内容;只提供 collection 时由它承载。
XhNavigationMenuRootchildrenReactNode
XhNavigationMenuTriggervaluestring是
XhNavigationMenuTriggerdisabledboolean默认交给 connect 查询 collection,写死 false 会覆盖数据中的禁用。
XhNavigationMenuTriggerIndicatorvaluestring是
XhNavigationMenuTriggerIndicatordisabledboolean

状态 ​

公开状态写入 data-state。

部件取值
root'open' | 'closed'
trigger'open' | 'closed'
trigger-indicator'open' | 'closed'
content'open' | 'closed'
branch-trigger'open' | 'closed'
branch-indicator'open' | 'closed'
branch-content'open' | 'closed'
indicator'open' | 'closed'
viewport'open' | 'closed'

以下名称仅用于内部状态机。

状态:idle · opening · skipping

事件:TRIGGER.POINTER · TRIGGER.FOCUS · TRIGGER.TOGGLE · DISMISS · VALUE.SET · PRESENCE.SET · after.delayDuration · after.skipDelayDuration · PRESS.START · PRESS.END · BRANCH.TOGGLE · BRANCH.DISMISS

判据:hasValue · isCurrent · shouldKeepOpen · canPress

connect API ​

getXxxProps() 返回对应部件的宿主属性。

成员类型说明
valuestring | null当前展开的项;全部收起时为 null。
collectionreadonly NavigationMenuNodeMeta[]由 collection 推导的入口元信息,按数据顺序排列;未提供 collection 时为空数组。
openboolean是否有面板展开。
isOpen(value: string) => boolean
branchValuestring | null面板里展开着的那一枝子级;都收着时为 null。
isBranchOpen(value: string) => boolean
setValue(next: string | null) => void
getRootProps() => T['element']
getListProps() => T['element']
getItemProps() => T['element']
getTriggerProps(props: NavigationMenuTriggerProps) => T['button']
getTriggerIndicatorProps(props: NavigationMenuTriggerProps) => T['element']
getContentProps(props: NavigationMenuContentProps) => T['element']
getBranchTriggerProps(props: NavigationMenuTriggerProps) => T['button']面板里一枝子级的开关:身份与禁用的声明同入口。
getBranchIndicatorProps(props: NavigationMenuContentProps) => T['element']子级开关里的展开方向标记,按 value 与所在的开关配对。
getBranchContentProps(props: NavigationMenuContentProps) => T['element']一枝子级的容器,按 value 与它的开关配对,收着时 hidden。
getLinkProps(props: NavigationMenuLinkProps) => T['element']
getIndicatorProps() => T['element']
getViewportProps() => T['element']

无障碍 ​

键盘 ​

规格出处:W3C APG

按键生效条件行为
Enter / Spaceheld on trigger / branch-trigger / link, 导航未禁用且条目未禁用按住期间入口、子级开关或面板链接投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,子级开关与链接随面板收起一并撤下。开合与激活语义照旧由这一次按键承担
ArrowRight / ArrowDownfocus in trigger, 按键与 orientation 同轴焦点移到下一个 trigger(禁用项跳过、尽头按 loop 回绕);随后的自动展开走 delayDuration
ArrowLeft / ArrowUpfocus in trigger, 按键与 orientation 同轴焦点移到上一个 trigger
Homefocus in trigger焦点移到首个可停留 trigger
Endfocus in trigger焦点移到末个可停留 trigger
Enter / Spacefocus in trigger, not disabled立即展开对应面板(不走 delayDuration);面板是自动弹出来的那一次不收起,再按一次才收起
Enter / Spacefocus in branch-trigger, not disabled展开或收起面板里的这一枝子级,焦点留在开关上;同一张面板只展开一枝,展开这一枝时另一枝收起。子级紧跟在开关之后,展开后 Tab 走进去,收着时带 hidden 被整段跳过
Escapefocus in 展开的 branch-content只收起这一枝子级并把焦点归还它的 branch-trigger,面板仍开着;再按一次 Escape 才收起面板
Escapeopen, 焦点不在展开的 branch-content 里收起面板并把焦点归还对应 trigger;静默窗口内这一次归还不会把面板重新弹出来
Tab / Shift+Tabopen, focus in trigger走进展开的面板:面板就在 trigger 之后,收起的面板带 hidden 因而被整个跳过

ARIA ​

以下属性由 connect 生成。

部件属性值
rootaria-labelresolveTranslations(NAVIGATION_MENU_EN_US, prop('tran…
triggeraria-controlscontent 部件的 id
triggeraria-disabled'true' | 'false'
triggeraria-expanded'true' | 'false'
trigger-indicatoraria-hidden'true'
contentaria-hidden!isOpen || undefined
contentaria-labelledbytrigger 部件的 id
contentrole'group'
branch-triggeraria-controlsbranch-content 部件的 id
branch-triggeraria-disabled'true' | 'false'
branch-triggeraria-expanded'true' | 'false'
branch-indicatoraria-hidden'true'
branch-contentaria-labelledbybranch-trigger 部件的 id
branch-contentrole'group'
linkaria-current'page' | undefined
indicatoraria-hidden'true'
viewportaria-hidden!open || undefined
  • 子级开关是原生按钮,aria-expanded / aria-controls 指向子级;子级收着时带 hidden、被 Tab 整段跳过,展开后 Tab 顺着文档序走进去。
  • 焦点在子级里按 Escape 只收起这一枝、焦点回到它的开关,再按一次才收起面板。

样式参考 ​

皮肤 ​

@xihan-ui/styles/navigation-menu.css 按 [data-scope="navigation-menu"][data-part="root"] 部件选择器书写,发布产物以挂载类 .xh-scope-navigation-menu 代替其中的 data-scope(特异性相同),位于 xihan.components 层。覆盖样式使用 xihan.overrides,部件选择器照常可用。

forced-colors: active 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。

数据属性 ​

由 connect 生成;条件不成立时不输出无值属性。

部件属性值
rootdata-orientationprops.orientation
rootdata-sizeprops.size
rootdata-state'open' | 'closed'
rootdata-toneprops.tone
listdata-orientationprops.orientation
triggerdata-disabled''(条件成立时才出现)
triggerdata-in-path''(条件成立时才出现)
triggerdata-orientationprops.orientation
triggerdata-pressed''(条件成立时才出现)
triggerdata-state'open' | 'closed'
triggerdata-xh-collection-context'nav'
triggerdata-xh-collection-item''
triggerdata-xh-collection-sizeprops.size
trigger-indicatordata-disabled''(条件成立时才出现)
trigger-indicatordata-orientationprops.orientation
trigger-indicatordata-state'open' | 'closed'
contentdata-instant''(条件成立时才出现)
contentdata-orientationprops.orientation
contentdata-state'open' | 'closed'
contentdata-switching''(条件成立时才出现)
branch-triggerdata-disabled''(条件成立时才出现)
branch-triggerdata-pressed''(条件成立时才出现)
branch-triggerdata-state'open' | 'closed'
branch-triggerdata-xh-collection-context'nav'
branch-triggerdata-xh-collection-item''
branch-triggerdata-xh-collection-sizeprops.size
branch-indicatordata-state'open' | 'closed'
branch-contentdata-state'open' | 'closed'
linkdata-current''(条件成立时才出现)
linkdata-pressed''(条件成立时才出现)
linkdata-xh-collection-context'nav'
linkdata-xh-collection-item''
linkdata-xh-collection-sizeprops.size
indicatordata-instant''(条件成立时才出现)
indicatordata-orientationprops.orientation
indicatordata-state'open' | 'closed'
indicatordata-valuecontext.get('value')
viewportdata-instant''(条件成立时才出现)
viewportdata-orientationprops.orientation
viewportdata-state'open' | 'closed'

CSS 变量 ​

本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。

变量部件CSS 属性状态默认来源说明
--xh-navigation-menu-branch-content-indentbranch-contentpadding-inline-startdefault--xh-space-4navigation-menu 的 branch-content 部件 padding-inline-start 覆盖槽。
--xh-navigation-menu-branch-indicator-sizebranch-indicator--xh-icon-size
block-size
inline-size
default--xh-control-indicator-sizenavigation-menu 的 branch-indicator 部件 --xh-icon-size、block-size、inline-size 覆盖槽。
--xh-navigation-menu-branch-trigger-gapbranch-triggergapdefault--xh-_navigation-menu-trigger-gapnavigation-menu 的 branch-trigger 部件 gap 覆盖槽。
--xh-navigation-menu-content-bgcontent
viewport
backgrounddefault--xh-bg-surfacenavigation-menu 的 content、viewport 部件 background 覆盖槽。
--xh-navigation-menu-content-bordercontent
viewport
borderdefault--xh-border-defaultnavigation-menu 的 content、viewport 部件 border 覆盖槽。
--xh-navigation-menu-content-gapbranch-content
content
gapdefault--xh-space-1navigation-menu 的 branch-content、content 部件 gap 覆盖槽。
--xh-navigation-menu-content-max-wcontent
viewport
max-inline-sizedefault--xh-overlay-max-w-xlnavigation-menu 的 content、viewport 部件 max-inline-size 覆盖槽。
--xh-navigation-menu-content-min-wcontentmin-inline-sizedefault--xh-overlay-menu-min-wnavigation-menu 的 content 部件 min-inline-size 覆盖槽。
--xh-navigation-menu-content-offsetcontent
viewport
inset-block-start
inset-inline-start
default
orientation=vertical
--xh-space-1navigation-menu 的 content、viewport 部件 inset-block-start、inset-inline-start 覆盖槽。
--xh-navigation-menu-content-pcontentpaddingdefault--xh-surface-pad-xsnavigation-menu 的 content 部件 padding 覆盖槽。
--xh-navigation-menu-content-radiuscontent
viewport
border-radiusdefault--xh-shape-overlaynavigation-menu 的 content、viewport 部件 border-radius 覆盖槽。
--xh-navigation-menu-content-shadowcontent
viewport
box-shadowdefault--xh-elevation-floatingnavigation-menu 的 content、viewport 部件 box-shadow 覆盖槽。
--xh-navigation-menu-fgrootcolordefault--xh-fg-defaultnavigation-menu 的 root 部件 color 覆盖槽。
--xh-navigation-menu-font-sizeroot
trigger
font-sizedefault--xh-_navigation-menu-font-sizenavigation-menu 的 root、trigger 部件 font-size 覆盖槽。
--xh-navigation-menu-gaplistgapdefault--xh-space-1navigation-menu 的 list 部件 gap 覆盖槽。
--xh-navigation-menu-icon-sizeroot--xh-icon-sizedefault
size=lg
size=sm
--xh-glyph-size-lg
--xh-glyph-size-md
--xh-glyph-size-sm
navigation-menu 的 root 部件 --xh-icon-size 覆盖槽。
--xh-navigation-menu-indicator-colorindicator
link
backgroundcurrent
default
--xh-_navigation-menu-accentnavigation-menu 的 indicator、link 部件 background 覆盖槽。
--xh-navigation-menu-indicator-radiusindicator
link
border-radiuscurrent
default
--xh-shape-pillnavigation-menu 的 indicator、link 部件 border-radius 覆盖槽。
--xh-navigation-menu-indicator-thicknessindicator
item
link
list
block-size
inline-size
inset-block-end
current
default
orientation=horizontal
orientation=vertical
--xh-stroke-thicknavigation-menu 的 indicator、item、link、list 部件 block-size、inline-size、inset-block-end 覆盖槽。
--xh-navigation-menu-layercontent
viewport
z-indexdefault--xh-_layernavigation-menu 的 content、viewport 部件 z-index 覆盖槽。
--xh-navigation-menu-link-bg-hoverbranch-trigger
link
background-colordisabled
error
highlighted
hover
is(:focus-visible, [data-highlighted])
not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])
xh-collection-context=nav
--xh-_navigation-menu-highlight-bgnavigation-menu 的 branch-trigger、link 部件 background-color 覆盖槽。
--xh-navigation-menu-link-bg-pressedbranch-trigger
link
background-colordisabled
error
is(:active, [data-pressed])
not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])
pressed
xh-collection-context=nav
--xh-bg-subtle-hovernavigation-menu 的 branch-trigger、link 部件 background-color 覆盖槽。
--xh-navigation-menu-link-fgbranch-trigger
link
colordefault
disabled
error
highlighted
hover
is(:active, [data-pressed])
is(:focus-visible, [data-highlighted])
not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])
pressed
xh-collection-context=nav
--xh-fg-defaultnavigation-menu 的 branch-trigger、link 部件 color 覆盖槽。
--xh-navigation-menu-link-fg-currentlinkcolorcurrent
disabled
error
not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])
xh-collection-context=nav
--xh-fg-brandnavigation-menu 的 link 部件 color 覆盖槽。
--xh-navigation-menu-link-font-sizebranch-trigger
link
font-sizedefault--xh-_navigation-menu-link-font-sizenavigation-menu 的 branch-trigger、link 部件 font-size 覆盖槽。
--xh-navigation-menu-link-font-weight-currentlinkfont-weightcurrent
disabled
error
not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])
xh-collection-context=nav
--xh-font-weight-mediumnavigation-menu 的 link 部件 font-weight 覆盖槽。
--xh-navigation-menu-link-pxbranch-trigger
link
padding-inlinedefault--xh-_navigation-menu-link-pxnavigation-menu 的 branch-trigger、link 部件 padding-inline 覆盖槽。
--xh-navigation-menu-link-pybranch-trigger
link
padding-blockdefault--xh-_navigation-menu-link-pynavigation-menu 的 branch-trigger、link 部件 padding-block 覆盖槽。
--xh-navigation-menu-link-radiusbranch-trigger
link
border-radiusdefault--xh-shape-controlnavigation-menu 的 branch-trigger、link 部件 border-radius 覆盖槽。
--xh-navigation-menu-trigger-bg-activetriggerbackground-colorin-path
xh-collection-context=nav
--xh-_navigation-menu-highlight-bgnavigation-menu 的 trigger 部件 background-color 覆盖槽。
--xh-navigation-menu-trigger-bg-hovertriggerbackground-colordisabled
error
hover
not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])
xh-collection-context=nav
--xh-_navigation-menu-highlight-bgnavigation-menu 的 trigger 部件 background-color 覆盖槽。
--xh-navigation-menu-trigger-bg-pressedtriggerbackground-colordisabled
error
is(:active, [data-pressed])
not([aria-disabled='true'], [data-disabled], [aria-busy='true'], [data-error])
pressed
xh-collection-context=nav
--xh-bg-subtle-hovernavigation-menu 的 trigger 部件 background-color 覆盖槽。
--xh-navigation-menu-trigger-fgtriggercolordefault
xh-collection-context=nav
--xh-fg-mutednavigation-menu 的 trigger 部件 color 覆盖槽。
--xh-navigation-menu-trigger-font-weighttriggerfont-weightdefault
xh-collection-context=nav
--xh-font-weight-regularnavigation-menu 的 trigger 部件 font-weight 覆盖槽。
--xh-navigation-menu-trigger-gaptriggergapdefault--xh-_navigation-menu-trigger-gapnavigation-menu 的 trigger 部件 gap 覆盖槽。
--xh-navigation-menu-trigger-htriggerblock-sizedefault--xh-_navigation-menu-trigger-hnavigation-menu 的 trigger 部件 block-size 覆盖槽。
--xh-navigation-menu-trigger-pxtriggerpadding-inlinedefault--xh-_navigation-menu-trigger-pxnavigation-menu 的 trigger 部件 padding-inline 覆盖槽。
--xh-navigation-menu-trigger-radiustriggerborder-radiusdefault--xh-shape-controlnavigation-menu 的 trigger 部件 border-radius 覆盖槽。
--xh-navigation-menu-viewport-pviewportpaddingdefault--xh-space-2navigation-menu 的 viewport 部件 padding 覆盖槽。

动效 ​

动效角色:按压 · 状态 · 切换 · 指示与换位 · 出现(见动效规范)。

共享关键帧 xh-fade-in · xh-fade-out · xh-overlay-pop-in · xh-pop-out 由 family/motion.css 提供,皮肤 @import 它,单独引入仍成立;block-size · inline-size · rotate · translate 走 transition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。

皮肤之外还有一段:退场由适配器的退场闸门把关,动画播完才真收起。

系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。

RTL ​

皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像;只认物理方向的量乘 --xh-direction-sign 换向,按就近的 dir 走;另有按 dir 分支的规则。

Released under The MIT License