Beta, expect API change, and bugs. Hit one? Tell us.

Skip to content

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>

<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.

PropTypeDefault
AttributeDescription
data-nestedPresent when this navigation menu is nested in another.
data-openPresent when a menu panel is open.

List

Contains a list of navigation menu items. Renders a <ul> element.

PropTypeDefault
AttributeDescription
data-openPresent when a menu panel is open.

Item

An individual navigation menu item. Renders a <li> element.

PropTypeDefault

Trigger

Opens its item's content on hover or click. Renders a <button> element.

PropTypeDefault
AttributeDescription
data-popup-openPresent while this item's content is showing.
data-pressedPresent 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.

PropTypeDefault
AttributeDescription
data-popup-openPresent 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.

PropTypeDefault
AttributeDescription
data-openPresent when this item's content is showing.
data-closedPresent when it is not.
data-activation-directionDirection the newly activated trigger sits in: left, right, up or down.
data-starting-stylePresent when the content is animating in.
data-ending-stylePresent when the content is animating out.

A link to another page or section. Renders an <a> element.

PropTypeDefault
AttributeDescription
data-activePresent when the link is the currently active page.

Backdrop

An overlay displayed beneath the popup. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-starting-stylePresent when the backdrop is animating in.
data-ending-stylePresent 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.

PropTypeDefault

Positioner

Positions the navigation menu against the currently active trigger. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-anchor-hiddenPresent when the anchor is hidden.
data-instantPresent when animations should be instant.
CSS VariableDescription
--available-widthAvailable width between the anchor and the viewport edge.
--available-heightAvailable height between the anchor and the viewport edge.
--anchor-widthWidth of the anchor element.
--anchor-heightHeight of the anchor element.
--transform-originTransform origin for scale animations.
--positioner-widthThe current panel's width in pixels. Set for as long as the navigation menu is open.
--positioner-heightThe 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.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-anchor-hiddenPresent when the anchor is hidden.
data-alignHow the popup is aligned relative to the side.
data-sideWhich side of the anchor the popup is on.
data-starting-stylePresent when the popup is animating in.
data-ending-stylePresent when the popup is animating out.
CSS VariableDescription
--popup-widthThe popup element's width: a pixel value while <NavigationMenu.Viewport> morphs between panels and again while the popup closes, auto once it settles.
--popup-heightThe 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.

PropTypeDefault

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.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-uncenteredPresent when the arrow cannot be centered.