shadcn-vue Navigation Menu 组件实战:基于 reka-ui 构建可访问的网站导航栏
【免费下载链接】shadcn-vueVue port of shadcn-ui项目地址: https://gitcode.com/gh_mirrors/sh/shadcn-vue
Navigation Menu 是 shadcn-vue 提供的用于网站导航的组件集合,它基于 reka-ui 的 Navigation Menu 原语封装,支持键盘导航、焦点管理、视口对齐与内容指示器等无障碍交互能力。本指南以 navigation-menu.md 为核心,结合仓库内 default 风格的完整源码实现,讲解安装方式、组件 API 组成、基础用法、navigationMenuTriggerStyle()在 NuxtLink 场景下的应用,以及如何编写一个带巨型菜单(mega menu)的实战示例。读完本文,你将能够独立地在 Nuxt 3 / Vite + Vue 3 项目中集成一套完整的响应式导航菜单。
组件概述:Navigation Menu 解决了什么问题
网站导航(尤其是文档站、组件库、SaaS 官网的多级菜单)往往面临几个棘手问题:
- 焦点管理:用 Tab 键在菜单项、子面板、链接之间移动时,焦点顺序必须符合可访问性规范;
- 键盘交互:方向键切换菜单项、Enter/Space 展开收起、Esc 关闭菜单;
- 定位与视口:子面板在不同屏幕宽度下的对齐方式、激活状态指示器的位置同步;
- 触摸设备:hover 触发的菜单在触屏上需要可用的降级方案。
shadcn-vue 的 Navigation Menu 组件集合把这些问题封装进 8 个相互配合的原子组件中,底层直接复用了 reka-ui 的NavigationMenuRoot、NavigationMenuList、NavigationMenuTrigger、NavigationMenuContent、NavigationMenuLink、NavigationMenuItem、NavigationMenuIndicator、NavigationMenuViewport原语,再叠加 Tailwind CSS 完成视觉与动效。仓库中 default 与 new-york 两套风格各维护一份同名实现,本文以 default 风格源码 为例展开。
安装 Navigation Menu
在 Vue 3 + Vite/Nuxt 项目中,通过 shadcn-vue CLI 一条命令即可安装全部相关组件:
npx shadcn-vue@latest add navigation-menu命令执行后,会在src/components/ui/navigation-menu/目录下生成以下 8 个.vue组件文件与一个 index.ts 聚合导出文件:
| 组件文件 | 对应 reka-ui 原语 | 职责 |
|---|---|---|
| NavigationMenu.vue | NavigationMenuRoot | 根容器,管理打开状态与内部视口 |
| NavigationMenuList.vue | NavigationMenuList | 菜单项列表容器 |
| NavigationMenuItem.vue | NavigationMenuItem | 单个菜单项(可含触发器和子面板) |
| NavigationMenuTrigger.vue | NavigationMenuTrigger | 可展开子菜单的触发按钮 |
| NavigationMenuContent.vue | NavigationMenuContent | 弹出的子面板内容 |
| NavigationMenuLink.vue | NavigationMenuLink | 普通导航链接 |
| NavigationMenuIndicator.vue | NavigationMenuIndicator | 当前激活项的指示条 |
| NavigationMenuViewport.vue | NavigationMenuViewport | 承载所有子面板的视口容器 |
index.ts还额外导出了navigationMenuTriggerStyle(),这是一个由class-variance-authority的cva生成的样式函数,供「无子菜单的普通链接」复用触发器同款视觉样式。
组件基础用法
按官方文档的标准用法,先把 8 个组件一次性引入:
<script setup lang="ts"> import { NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, } from '@/components/ui/navigation-menu' </script> <template> <NavigationMenu> <NavigationMenuList> <NavigationMenuItem> <NavigationMenuTrigger>Item One</NavigationMenuTrigger> <NavigationMenuContent> <NavigationMenuLink>Link</NavigationMenuLink> </NavigationMenuContent> </NavigationMenuItem> </NavigationMenuList> </NavigationMenu> </template>结构上的关键点:
NavigationMenu必须作为最外层根组件。查看 NavigationMenu.vue 的实现可以发现,它除了渲染NavigationMenuRoot外,还自动在内部末尾渲染了<NavigationMenuViewport />,因此你不必(也不应)手动再放置一个 Viewport,否则会出现两个视口实例;- 根组件默认样式为
relative z-10 flex max-w-max flex-1 items-center justify-center,保证菜单浮层相对定位正确且不被遮挡; - 有子面板的菜单项:
NavigationMenuItem内放NavigationMenuTrigger+NavigationMenuContent; - 纯链接菜单项:
NavigationMenuItem内直接放NavigationMenuLink(配合navigationMenuTriggerStyle()统一外观,见下文)。
源码级解析:组件如何包装 reka-ui
shadcn-vue 组件的通用包装模式是「透传 props/emits + 合并 class」。以触发器为例,NavigationMenuTrigger.vue 的关键逻辑:
const props = defineProps<NavigationMenuTriggerProps & { class?: HTMLAttributes["class"] }>() const delegatedProps = reactiveOmit(props, "class") const forwardedProps = useForwardProps(delegatedProps)reactiveOmit(props, "class")把class从 props 中剥离,避免它被原样透传给底层原语;useForwardProps将剩余 props 转换为响应式转发对象,v-bind后传给 reka-ui 的NavigationMenuTrigger;- 视觉部分则通过
cn(navigationMenuTriggerStyle(), 'group', props.class)合并:既应用统一触发器样式,又保留调用方的自定义 class 覆盖能力。
触发器内部还内置了一个ChevronDown箭头图标(lucide-vue-next),并在group-data-[state=open]:rotate-180条件下旋转 180°,配合transition duration-200实现展开时箭头翻转的动效。
再看 NavigationMenuContent.vue,它使用 reka-ui 在子面板上注入的data-motion状态属性来控制进出场动画:
data-[motion^=from-]:animate-in>h-[--reka-navigation-menu-viewport-height] w-full md:w-[--reka-navigation-menu-viewport-width] left-[var(--reka-navigation-menu-viewport-left)]面板高度、宽度、左偏移全部由 reka-ui 根据当前激活项动态计算并写入 CSS 变量,从而实现「视口与当前打开的面板尺寸自动同步」;外层再用origin-top-center+zoom-in-90/zoom-out-95完成缩放渐变动画。
NavigationMenuIndicator.vue 渲染在面板上方、触发器下方,用一个小旋转 45° 的bg-border方块充当「箭头指示器」,并通过data-[state=visible]/data-[state=hidden]控制淡入淡出,指示条的位置同样跟随 reka-ui 自动计算。
让链接与触发器风格统一:navigationMenuTriggerStyle
在文档站里,导航栏中既有「可展开的分类菜单」,也有「直接跳转的普通链接」(如 "Documentation"、"GitHub")。为了让二者视觉一致,index.ts导出了navigationMenuTriggerStyle():
export const navigationMenuTriggerStyle = cva( "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50>import { navigationMenuTriggerStyle } from '@/components/ui/navigation-menu'<template> <NavigationMenuItem> <NuxtLink v-slot="{ isActive, href, navigate }" to="/docs" custom> <NavigationMenuLink :active="isActive" :href :class="navigationMenuTriggerStyle()" @click="navigate" > Documentation </NavigationMenuLink> </NuxtLink> </NavigationMenuItem> </template>这里的关键细节:
v-slot="{ isActive, href, navigate }"从<NuxtLink>取回当前路由激活状态、解析后的 href 和编程式导航函数;:active="isActive"把激活态同步给 reka-ui 的NavigationMenuLink(对应上方样式里的data-active高亮);:href负责将解析好的真实地址回填;@click="navigate"接管点击跳转,保证 Nuxt 的客户端路由与预取逻辑不丢失;- 最终用
navigationMenuTriggerStyle()统一按钮外观。
实战示例:构建带巨型菜单的文档导航栏
仓库中的 NavigationMenuDemo.vue(同时存在于 default 与 new-york 风格)演示了一个完整的巨型菜单导航栏,其结构非常典型,可直接迁移到文档站点。它由三个菜单项组成:
- Getting started(巨型菜单):
NavigationMenuContent内放一个ul.grid,左侧是一个跨 3 行、带渐变背景的品牌介绍卡(通过as-child把NavigationMenuLink直接挂在原生<a>上),右侧是 Introduction / Installation / Typography 三个文档入口; - Components(网格菜单):用
v-for遍历组件数组,渲染成md:grid-cols-2 lg:grid-cols-...的多列网格,每项是带标题 + 两行描述(line-clamp-2)的链接卡; - Documentation(纯链接):不包含
NavigationMenuContent,直接使用NavigationMenuLink配合navigationMenuTriggerStyle()渲染。
值得复用的模式要点:
as-child属性:当链接需要直接作用在自定义元素(原生<a>、<NuxtLink>、<RouterLink>)上时,给NavigationMenuLink加as-child,再在内部放置目标元素,reka-ui 会把自身属性与事件合并到该元素上;- 面板内布局自由:
NavigationMenuContent只是定位与动画容器,内部完全可以用 Tailwind 的grid、gap、p-6等自由排版,宽度用md:w-[400px]/lg:w-[500px]等响应式断点控制; - 链接样式统一:列表中的链接卡统一使用
block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground这一组类名,保证 hover/focus 态一致且可访问。
无障碍与键盘交互(由底层原语保障)
可访问性能力并非 shadcn-vue 自行实现,而是继承自 reka-ui 的 NavigationMenu 原语(官方文档 中可查),包括:
- 触发器/菜单项自动带上
aria-haspopup、aria-expanded等语义属性; - 子面板的
role、aria-orientation正确标注,支持屏幕阅读器; - 完整键盘支持:
ArrowLeft/ArrowRight在菜单项间移动,ArrowUp/ArrowDown在子菜单内移动,Enter/Space展开,Esc关闭并归还焦点,Tab移入/移出; - 指针在触发器与面板之间移动时的「安全区」处理,避免误关闭。
因此在使用时不要删除 reka-ui 原语上的默认属性,只需像上文那样叠加 class 与自定义内容即可。
小结与延伸
Navigation Menu 组件集合展示了 shadcn-vue 的典型架构:reka-ui 提供行为与可访问性原语,cva 提供样式系统,reactiveOmit+useForwardProps(Emits)实现 props 透传,Tailwind 的data-*变体实现状态驱动的动效。掌握这套模式后,你不仅可以熟练使用导航菜单,也能举一反三地理解仓库中其他同类组件的实现。
若需进一步自定义,可以阅读仓库内的两套实现:
- default 风格:deprecated/www/src/registry/default/ui/navigation-menu/
- new-york 风格:deprecated/www/src/registry/new-york/ui/navigation-menu/
以及完整演示示例 NavigationMenuDemo.vue。修改navigationMenuTriggerStyle()中的 cva 字符串或组件内的 Tailwind 类名即可调整尺寸、圆角、颜色与动画,且不会影响任何键盘与无障碍行为。
【免费下载链接】shadcn-vueVue port of shadcn-ui项目地址: https://gitcode.com/gh_mirrors/sh/shadcn-vue
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考