Navigation Menu
A navigation menu with floating panels.
<script setup lang="ts">
import { NavigationMenu } from '@shardsui/vue/navigation-menu'
type LinkItem = { href: string; title: string; description: string }
const topics: LinkItem[] = [
{
href: '#',
title: 'Frontend',
description: 'From first components to full working apps.'
},
{ href: '#', title: 'Design', description: 'Type scale, color tokens, and layout grids.' },
{
href: '#',
title: 'Accessibility',
description: 'Semantic HTML, ARIA, and keyboard nav.'
},
{ href: '#', title: 'Motion', description: 'Easing, timing, and interface animation.' }
]
const resources: LinkItem[] = [
{
href: '#',
title: 'Docs',
description: 'Guides and reference for every feature.'
},
{ href: '#', title: 'Changelog', description: 'What shipped and when.' },
{ href: '#', title: 'Blog', description: 'Updates, tips, and release notes.' }
]
const triggerClass =
'flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100 min-[501px]:px-3'
const contentClass =
'h-full w-[calc(100vw-40px)] p-4 transition-[opacity,translate] duration-[calc(var(--duration)*0.5),var(--duration)] ease-[ease,var(--easing)] data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-1/2 data-starting-style:data-[activation-direction=left]:-translate-x-1/2 data-ending-style:data-[activation-direction=right]:-translate-x-1/2 data-starting-style:data-[activation-direction=right]:translate-x-1/2 min-[500px]:w-max min-[500px]:min-w-100'
const cardClass =
'relative block size-full rounded-md p-2 text-left text-inherit no-underline hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 min-[501px]:p-3'
</script>
<template>
<NavigationMenu.Root class="min-w-max rounded-lg bg-gray-50 p-1 text-gray-900">
<NavigationMenu.List class="relative flex gap-px">
<NavigationMenu.Item>
<NavigationMenu.Trigger :class="triggerClass">
Products
<NavigationMenu.Icon
class="transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5.75 9.5L12 15.75L18.25 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content :class="contentClass">
<ul class="grid list-none grid-cols-1 min-[500px]:grid-cols-[13rem_13rem]">
<li v-for="item in topics" :key="item.title">
<NavigationMenu.Link :href="item.href" :class="cardClass">
<h3 class="m-0 mb-1 text-sm leading-4 font-normal">{{ item.title }}</h3>
<p class="m-0 text-sm/5 text-gray-500">{{ item.description }}</p>
</NavigationMenu.Link>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger :class="triggerClass">
Resources
<NavigationMenu.Icon
class="transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5.75 9.5L12 15.75L18.25 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content :class="contentClass">
<ul class="flex max-w-100 list-none flex-col justify-center">
<li v-for="item in resources" :key="item.title">
<NavigationMenu.Link :href="item.href" :class="cardClass">
<h3 class="m-0 mb-1 text-sm leading-4 font-normal">{{ item.title }}</h3>
<p class="m-0 text-sm/5 text-gray-500">{{ item.description }}</p>
</NavigationMenu.Link>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link :class="triggerClass" href="#">Pricing</NavigationMenu.Link>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
:side-offset="10"
:collision-padding="{ top: 5, bottom: 5, left: 20, right: 20 }"
:collision-avoidance="{ side: 'none' }"
style="--duration: 0.35s; --easing: cubic-bezier(0.22, 1, 0.36, 1)"
class="box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-(--duration) ease-(--easing) before:absolute before:content-[''] data-instant:transition-none data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:-top-2.5 data-[side=bottom]:before:h-2.5 data-[side=left]:before:inset-y-0 data-[side=left]:before:-right-2.5 data-[side=left]:before:w-2.5 data-[side=right]:before:inset-y-0 data-[side=right]:before:-left-2.5 data-[side=right]:before:w-2.5 data-[side=top]:before:inset-x-0 data-[side=top]:before:-bottom-2.5 data-[side=top]:before:h-2.5"
>
<NavigationMenu.Popup
class="relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-gray-50 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[opacity,transform,width,height,scale] duration-(--duration) ease-(--easing) data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-ending-style:ease-[ease] data-starting-style:scale-90 data-starting-style:opacity-0"
>
<NavigationMenu.Arrow
class="flex transition-[left,right] duration-(--duration) ease-(--easing) data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
class="fill-gray-50"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
class="fill-gray-200"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
/>
</svg>
</NavigationMenu.Arrow>
<NavigationMenu.Viewport class="relative size-full overflow-hidden rounded-[inherit]" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</template>Anatomy
<script setup>
import { NavigationMenu } from '@shardsui/vue/navigation-menu'
</script>
<template>
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>
<NavigationMenu.Icon />
</NavigationMenu.Trigger>
<NavigationMenu.Content>
<NavigationMenu.Link />
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Backdrop />
<NavigationMenu.Positioner>
<NavigationMenu.Popup>
<NavigationMenu.Arrow />
<NavigationMenu.Viewport />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</template>Examples
Nested submenus
Nest a <NavigationMenu.Root> inside a <NavigationMenu.Content> for a second level. The nested root brings its own Portal, Positioner and Viewport, so the submenu floats free of the parent panel.
<script setup lang="ts">
import { NavigationMenu } from '@shardsui/vue/navigation-menu'
type LinkItem = { href: string; title: string; description: string }
const topics: LinkItem[] = [
{
href: '#',
title: 'Frontend',
description: 'From first components to full working apps.'
},
{ href: '#', title: 'Design', description: 'Type scale, color tokens, and layout grids.' }
]
const resources: LinkItem[] = [
{
href: '#',
title: 'Docs',
description: 'Guides and reference for every feature.'
},
{ href: '#', title: 'Changelog', description: 'What shipped and when.' },
{ href: '#', title: 'Blog', description: 'Updates, tips, and release notes.' }
]
const contentClass =
'h-full w-[calc(100vw-40px)] p-4 transition-[opacity,transform,translate] duration-(--duration) ease-(--easing) data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-1/2 data-starting-style:data-[activation-direction=left]:-translate-x-1/2 data-ending-style:data-[activation-direction=right]:-translate-x-1/2 data-starting-style:data-[activation-direction=right]:translate-x-1/2 min-[32rem]:w-max min-[32rem]:min-w-100'
const positionerClass =
"box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-(--duration) ease-(--easing) before:absolute before:content-[''] data-instant:transition-none data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:-top-2.5 data-[side=bottom]:before:h-2.5 data-[side=left]:before:inset-y-0 data-[side=left]:before:-right-2.5 data-[side=left]:before:w-2.5 data-[side=right]:before:inset-y-0 data-[side=right]:before:-left-2.5 data-[side=right]:before:w-2.5 data-[side=top]:before:inset-x-0 data-[side=top]:before:-bottom-2.5 data-[side=top]:before:h-2.5"
const popupClass =
'relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-gray-50 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[opacity,transform,width,height,scale,translate] duration-(--duration) ease-(--easing) data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:duration-150 data-ending-style:ease-[ease] data-starting-style:scale-95 data-starting-style:opacity-0'
const cardClass =
'relative block w-full rounded-md p-2 text-left text-inherit no-underline hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 sm:p-3'
</script>
<template>
<NavigationMenu.Root class="min-w-max rounded-lg bg-gray-50 p-1 text-gray-900">
<NavigationMenu.List class="relative flex gap-px">
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="m-0 box-border flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm/6 font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100 sm:px-3 sm:text-sm"
>
Products
<NavigationMenu.Icon
class="transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5.75 9.5L12 15.75L18.25 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content :class="contentClass">
<ul class="grid list-none grid-cols-1 gap-0 sm:grid-cols-[12rem_12rem]">
<li v-for="item in topics" :key="item.title">
<NavigationMenu.Link :href="item.href" :class="cardClass">
<h3 class="m-0 mb-1 text-sm/5 font-normal">{{ item.title }}</h3>
<p class="m-0 text-sm/5 text-gray-500">{{ item.description }}</p>
</NavigationMenu.Link>
</li>
<li>
<NavigationMenu.Root orientation="vertical">
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="relative block w-full rounded-md p-2 text-left text-inherit no-underline hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-popup-open:bg-gray-100 sm:p-3"
>
<span class="m-0 mb-1 text-sm/5 font-normal">Resources</span>
<p class="m-0 text-sm/5 text-gray-500">Guides, changelog, and blog posts.</p>
<NavigationMenu.Icon
class="absolute top-1/2 right-2.5 flex size-4 -translate-y-1/2 items-center justify-center transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M9.5 18.25L15.75 12L9.5 5.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content :class="contentClass">
<ul class="flex max-w-100 flex-col justify-center">
<li v-for="item in resources" :key="item.title">
<NavigationMenu.Link :href="item.href" :class="cardClass">
<h3 class="m-0 mb-1 text-sm/5 font-normal">{{ item.title }}</h3>
<p class="m-0 text-sm/5 text-gray-500">{{ item.description }}</p>
</NavigationMenu.Link>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
:side-offset="8"
:align-offset="-8"
align="end"
side="right"
:class="positionerClass"
style="--duration: 0.35s; --easing: cubic-bezier(0.22, 1, 0.36, 1)"
>
<NavigationMenu.Popup :class="popupClass">
<NavigationMenu.Viewport class="relative size-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
:side-offset="10"
:collision-padding="{ top: 5, bottom: 5, left: 20, right: 20 }"
:class="positionerClass"
style="--duration: 0.35s; --easing: cubic-bezier(0.22, 1, 0.36, 1)"
>
<NavigationMenu.Popup :class="popupClass">
<NavigationMenu.Arrow
class="flex transition-[left] duration-(--duration) ease-(--easing) data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
class="fill-gray-50"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
class="fill-gray-200"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
/>
</svg>
</NavigationMenu.Arrow>
<NavigationMenu.Viewport class="relative size-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</template>Nested inline submenus
For second-level navigation that stays inside the same panel, give the nested root only a List and a Viewport — no Portal or Positioner — and seed the open submenu with value.
<script setup lang="ts">
import { NavigationMenu } from '@shardsui/vue/navigation-menu'
import { computed, onMounted, onUnmounted, shallowRef } from 'vue'
const collections = [
{
value: 'getting-started',
label: 'Getting started',
hint: 'Start from zero.',
title: 'Build a foundation',
description: 'Short guides for your first steps.',
links: [
{
href: '#',
title: 'Installation',
description: 'Set up the library in your project.'
},
{
href: '#',
title: 'Quick start',
description: 'Compose your first component.'
}
]
},
{
value: 'components',
label: 'Components',
hint: 'Explore the parts.',
title: 'Browse the catalog',
description: 'Every component, documented and ready to use.',
links: [
{
href: '#',
title: 'Dialog',
description: 'Modal overlays with focus management.'
},
{
href: '#',
title: 'Popover',
description: 'Floating panels anchored to a trigger.'
}
]
},
{
value: 'advanced',
label: 'Advanced',
hint: 'Go deeper.',
title: 'Customize behavior',
description: 'Handles, detached triggers, and composition patterns.',
links: [
{
href: '#',
title: 'Detached triggers',
description: 'Share one popup across multiple triggers.'
},
{
href: '#',
title: 'State factories',
description: 'How internal state is structured.'
}
]
}
]
const desktop = shallowRef(false)
let query: MediaQueryList | undefined
function syncDesktop() {
desktop.value = query?.matches ?? false
}
onMounted(() => {
query = window.matchMedia('(min-width: 700px)')
syncDesktop()
query.addEventListener('change', syncDesktop)
})
onUnmounted(() => query?.removeEventListener('change', syncDesktop))
const orientation = computed(() => (desktop.value ? 'vertical' : 'horizontal'))
</script>
<template>
<NavigationMenu.Root class="min-w-max rounded-lg bg-gray-50 p-1 text-gray-900">
<NavigationMenu.List class="relative flex gap-px">
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="m-0 box-border flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm/6 font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100 sm:px-3 sm:text-sm"
>
Browse
<NavigationMenu.Icon
class="transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5.75 9.5L12 15.75L18.25 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content
class="h-full w-[calc(100vw-40px)] p-0 transition-[opacity,translate] duration-[calc(var(--duration)*0.5),var(--duration)] ease-[ease,cubic-bezier(0.4,0,0.2,1)] data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--duration)*0.5)] data-ending-style:ease-[ease] data-ending-style:data-[activation-direction=left]:translate-x-8 data-starting-style:data-[activation-direction=left]:-translate-x-8 data-starting-style:data-[activation-direction=left]:opacity-0 data-ending-style:data-[activation-direction=right]:-translate-x-8 data-starting-style:data-[activation-direction=right]:translate-x-8 data-starting-style:data-[activation-direction=right]:opacity-0 min-[700px]:max-w-168.75"
>
<NavigationMenu.Root
class="overflow-hidden text-gray-900"
:orientation="orientation"
value="getting-started"
>
<div
class="grid grid-cols-1 overflow-clip rounded-lg min-[700px]:grid-cols-[13rem_minmax(0,1fr)]"
>
<NavigationMenu.List
class="m-0 flex list-none flex-row gap-1 overflow-x-auto bg-gray-100 p-4 min-[700px]:box-border min-[700px]:h-(--popup-height) min-[700px]:flex-col min-[700px]:gap-px min-[700px]:overflow-x-visible min-[700px]:overflow-y-auto min-[700px]:border-r min-[700px]:border-r-gray-200 min-[700px]:transition-[height] min-[700px]:duration-(--duration) min-[700px]:ease-(--easing)"
>
<NavigationMenu.Item
v-for="menu in collections"
:key="menu.value"
:value="menu.value"
>
<NavigationMenu.Trigger
class="m-0 box-border flex w-full min-w-40 flex-col items-start gap-0.5 rounded-lg bg-transparent px-3 py-2.5 text-left text-inherit hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-popup-open:bg-gray-50 data-popup-open:shadow-sm"
>
<span class="text-sm leading-tight font-normal text-gray-900">
{{ menu.label }}
</span>
<span class="text-sm leading-snug text-gray-500">{{ menu.hint }}</span>
</NavigationMenu.Trigger>
<NavigationMenu.Content
class="min-[700px]:blur-0 h-full translate-x-0 p-7 transition-[opacity,translate,filter] duration-(--duration) ease-(--easing) data-ending-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-1/2 data-starting-style:data-[activation-direction=left]:-translate-x-1/2 data-starting-style:data-[activation-direction=left]:opacity-0 data-ending-style:data-[activation-direction=right]:-translate-x-1/2 data-starting-style:data-[activation-direction=right]:translate-x-1/2 data-starting-style:data-[activation-direction=right]:opacity-0 min-[700px]:p-8 min-[700px]:duration-[calc(var(--duration)*1.35)] min-[700px]:ease-out-expo min-[700px]:data-ending-style:blur-[2px] min-[700px]:data-starting-style:blur-[2px] min-[700px]:data-ending-style:data-[activation-direction=down]:-translate-y-18 min-[700px]:data-starting-style:data-[activation-direction=down]:translate-y-18 min-[700px]:data-starting-style:data-[activation-direction=down]:opacity-0 min-[700px]:data-ending-style:data-[activation-direction=up]:translate-y-18 min-[700px]:data-starting-style:data-[activation-direction=up]:-translate-y-18 min-[700px]:data-starting-style:data-[activation-direction=up]:opacity-0"
>
<h4 class="m-0 text-base leading-tight font-normal">{{ menu.title }}</h4>
<p class="m-0 mt-2.5 text-sm text-gray-500">{{ menu.description }}</p>
<ul class="m-0 -mx-2 mt-4 grid list-none gap-0 p-0">
<li v-for="link in menu.links" :key="link.title">
<NavigationMenu.Link
class="box-border block rounded-lg bg-transparent px-2 py-3 text-inherit no-underline hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950"
:href="link.href"
>
<h5 class="m-0 text-sm/4.5 font-normal">{{ link.title }}</h5>
<p class="m-0 mt-1.5 text-sm leading-normal text-gray-500">
{{ link.description }}
</p>
</NavigationMenu.Link>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Viewport
class="relative min-h-66 overflow-hidden border-t border-gray-200 min-[700px]:border-t-0"
/>
</div>
</NavigationMenu.Root>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link
class="m-0 box-border flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm/6 font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 sm:px-3 sm:text-sm"
href="#"
>
Pricing
</NavigationMenu.Link>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
:side-offset="10"
:collision-padding="{ top: 5, bottom: 5, left: 20, right: 20 }"
:collision-avoidance="{ side: 'none' }"
class="box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-(--duration) ease-(--easing) before:absolute before:content-[''] data-instant:transition-none data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:-top-2.5 data-[side=bottom]:before:h-2.5 data-[side=left]:before:inset-y-0 data-[side=left]:before:-right-2.5 data-[side=left]:before:w-2.5 data-[side=right]:before:inset-y-0 data-[side=right]:before:-left-2.5 data-[side=right]:before:w-2.5 data-[side=top]:before:inset-x-0 data-[side=top]:before:-bottom-2.5 data-[side=top]:before:h-2.5"
style="--duration: 0.35s; --easing: cubic-bezier(0.22, 1, 0.36, 1)"
>
<NavigationMenu.Popup
class="relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-gray-50 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[opacity,scale,width,height] duration-(--duration) ease-(--easing) data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:transition-[opacity,scale] data-ending-style:duration-150 data-ending-style:ease-[ease] data-starting-style:scale-95 data-starting-style:opacity-0"
>
<NavigationMenu.Arrow
class="flex transition-[left] duration-(--duration) ease-(--easing) data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
class="fill-gray-50"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
class="fill-gray-200"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
/>
</svg>
</NavigationMenu.Arrow>
<NavigationMenu.Viewport class="relative size-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</template>Custom links
<NavigationMenu.Link> renders an <a>, so a plain href works for a full page load.
<template>
<NavigationMenu.Link href="/docs">Docs</NavigationMenu.Link>
</template>as takes an HTML tag name, not a component, so the router's own link can't be rendered through
it. For client-side navigation, render <RouterLink> as a renderless component and hand its href
and navigate to the part, so the element stays a real <a> for the browser and assistive tech:
<template>
<RouterLink v-slot="{ href, navigate }" to="/docs" custom>
<NavigationMenu.Link :href="href" @click="navigate">Docs</NavigationMenu.Link>
</RouterLink>
</template>The menu stays open after a link is clicked; add closeOnClick to dismiss it on navigation.
Large menus
When a panel is taller than the space below its trigger, cap it against --available-height so it shrinks instead of overflowing:
.content,
.popup {
max-height: var(--available-height);
}That only works if the content can compress. When it can't, let it scroll:
.content,
.popup {
max-height: var(--available-height);
}
.content {
overflow-y: auto;
}Native scrollbars stay visible while the panel transitions, so Scroll Area is the better fit here. It keeps the scrollbars hidden and lets the Arrow stay centered.
API reference
Root
Groups all parts of the navigation menu.
Renders a <nav> element at the root, or a <div> element when nested.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'nav' | |
class | string | — | |
style | string | — | |
value | unknown | null | |
update:value | (value: unknown) => void | — | |
delay | number | 50 | |
closeDelay | number | 50 | |
orientation | 'horizontal' | 'vertical' | 'horizontal' | |
openChangeComplete | (open: boolean) => void | — | |
default | Slot<{ open, nested }> | — | |
| Attribute | Description |
|---|---|
data-nested | Present when this navigation menu is nested in another. |
data-open | Present when a menu panel is open. |
List
Contains a list of navigation menu items.
Renders a <ul> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'ul' | |
class | string | — | |
style | string | — | |
default | Slot<{ open }> | — | |
| Attribute | Description |
|---|---|
data-open | Present when a menu panel is open. |
Item
An individual navigation menu item.
Renders a <li> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'li' | |
class | string | — | |
style | string | — | |
value | unknown | auto-generated | |
default | Slot | — | |
Trigger
Opens its item's content on hover or click.
Renders a <button> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'button' | |
class | string | — | |
style | string | — | |
disabled | boolean | false | |
default | Slot<{ open }> | — | |
| Attribute | Description |
|---|---|
data-popup-open | Present while this item's content is showing. |
data-pressed | Present alongside data-popup-open, however the content was opened. |
Icon
An icon that indicates that the trigger button opens a menu.
Renders a <span> element, hidden from assistive technology, falling back to a ▼ glyph when given no content.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'span' | |
class | string | — | |
style | string | — | |
default | Slot<{ open }> | — | |
| Attribute | Description |
|---|---|
data-popup-open | Present while this item's content is showing. |
Content
The panel for an item, rendered inside <NavigationMenu.Viewport> while that item is active.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
keepMounted | boolean | false | |
default | Slot<{ open, transitionStatus, activationDirection }> | — | |
| Attribute | Description |
|---|---|
data-open | Present when this item's content is showing. |
data-closed | Present when it is not. |
data-activation-direction | Direction the newly activated trigger sits in: left, right, up or down. |
data-starting-style | Present when the content is animating in. |
data-ending-style | Present when the content is animating out. |
Link
A link to another page or section.
Renders an <a> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'a' | |
class | string | — | |
style | string | — | |
href | string | — | |
active | boolean | false | |
closeOnClick | boolean | false | |
default | Slot<{ active }> | — | |
| Attribute | Description |
|---|---|
data-active | Present when the link is the currently active page. |
Backdrop
An overlay displayed beneath the popup.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
default | Slot<{ open, transitionStatus }> | — | |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-starting-style | Present when the backdrop is animating in. |
data-ending-style | Present when the backdrop is animating out. |
Portal
A portal that moves the popup out to <body>, clear of ancestor clipping and stacking.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
container | HTMLElement | null | — | |
keepMounted | boolean | false | |
default | Slot | — | |
Positioner
Positions the navigation menu against the currently active trigger.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
side | 'top' | 'bottom' | 'left' | 'right' | 'inline-start' | 'inline-end' | 'bottom' | |
align | 'start' | 'center' | 'end' | 'center' | |
sideOffset | number | OffsetFunction | 0 | |
alignOffset | number | OffsetFunction | 0 | |
collisionBoundary | 'clipping-ancestors' | Element | Element[] | Rect | 'clipping-ancestors' | |
collisionPadding | number | Padding | 5 | |
collisionAvoidance | CollisionAvoidance | { fallbackAxisSide: 'end' } when nested, { fallbackAxisSide: 'none' } otherwise | |
sticky | boolean | false | |
arrowPadding | number | 5 | |
disableAnchorTracking | boolean | false | |
anchor | Element | VirtualAnchorElement | null | active trigger | |
positionMethod | 'absolute' | 'fixed' | 'absolute' | |
default | Slot<{ open, side, align, anchorHidden, instant }> | — | |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-side | Which side of the anchor the popup is on. |
data-align | How the popup is aligned relative to the side. |
data-anchor-hidden | Present when the anchor is hidden. |
data-instant | Present when animations should be instant. |
| CSS Variable | Description |
|---|---|
--available-width | Available width between the anchor and the viewport edge. |
--available-height | Available height between the anchor and the viewport edge. |
--anchor-width | Width of the anchor element. |
--anchor-height | Height of the anchor element. |
--transform-origin | Transform origin for scale animations. |
--positioner-width | The current panel's width in pixels. Set for as long as the navigation menu is open. |
--positioner-height | The current panel's height in pixels. Set for as long as the navigation menu is open. |
Popup
A container for the navigation menu contents.
Renders a <nav> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'nav' | |
class | string | — | |
style | string | — | |
id | string | auto | |
default | Slot<{ open, transitionStatus, side, align, anchorHidden }> | — | |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-anchor-hidden | Present when the anchor is hidden. |
data-align | How the popup is aligned relative to the side. |
data-side | Which side of the anchor the popup is on. |
data-starting-style | Present when the popup is animating in. |
data-ending-style | Present when the popup is animating out. |
| CSS Variable | Description |
|---|---|
--popup-width | The popup element's width: a pixel value while <NavigationMenu.Viewport> morphs between panels and again while the popup closes, auto once it settles. |
--popup-height | The popup element's height: a pixel value while <NavigationMenu.Viewport> morphs between panels and again while the popup closes, auto once it settles. |
Viewport
The clipping viewport of the navigation menu's current content.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
id | string | auto | |
default | Slot | — | |
The active <NavigationMenu.Content> is rendered here, along with the previous one while it
animates out. Both carry the data attributes listed under Content.
Arrow
Displays an element positioned against the anchor.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
default | Slot<{ open, side, align, uncentered }> | — | |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-side | Which side of the anchor the popup is on. |
data-align | How the popup is aligned relative to the side. |
data-uncentered | Present when the arrow cannot be centered. |