Skip to main content
Nexus Design System
GitHub

NavigationMenu

NavigationMenu shows top-level site links, with flyout panels for groups of pages. Use it for a website header. For app commands, use Menubar.

'use client';

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/nxds/components/navigation-menu/navigation-menu';

export default function NavigationMenuDefault() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Products</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
              <li>
                <NavigationMenuLink href="#analytics">
                  Analytics
                </NavigationMenuLink>
              </li>
              <li>
                <NavigationMenuLink href="#automation">
                  Automation
                </NavigationMenuLink>
              </li>
              <li>
                <NavigationMenuLink href="#reports">Reports</NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Company</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
              <li>
                <NavigationMenuLink href="#about">About</NavigationMenuLink>
              </li>
              <li>
                <NavigationMenuLink href="#careers">Careers</NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  );
}

Installation

npx nxds-cli@latest add navigation-menu

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuIndicator,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/nxds/components/navigation-menu/navigation-menu';

Composition

NavigationMenu
└── NavigationMenuList
    ├── NavigationMenuItem
    │   ├── NavigationMenuTrigger
    │   ├── NavigationMenuContent
    │   │   └── NavigationMenuLink
    │   └── NavigationMenuLink
    └── NavigationMenuIndicator

Examples

Style a plain link with navigationMenuTriggerStyle() to sit beside triggers.

'use client';

import {
  NavigationMenu,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  navigationMenuTriggerStyle,
} from '@/components/nxds/components/navigation-menu/navigation-menu';

export default function NavigationMenuWithSimpleLink() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuLink
            href="#docs"
            className={navigationMenuTriggerStyle()}
          >
            Docs
          </NavigationMenuLink>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink
            href="#pricing"
            className={navigationMenuTriggerStyle()}
          >
            Pricing
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  );
}

Without viewport

Set viewport={false} to open each flyout under its own trigger.

'use client';

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/nxds/components/navigation-menu/navigation-menu';

export default function NavigationMenuWithoutViewport() {
  return (
    <NavigationMenu viewport={false}>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Products</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
              <li>
                <NavigationMenuLink href="#analytics">
                  Analytics
                </NavigationMenuLink>
              </li>
              <li>
                <NavigationMenuLink href="#reports">Reports</NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  );
}

With indicator

Add NavigationMenuIndicator to point an arrow at the open flyout.

'use client';

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuIndicator,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/nxds/components/navigation-menu/navigation-menu';

export default function NavigationMenuWithIndicator() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Products</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
              <li>
                <NavigationMenuLink href="#analytics">
                  Analytics
                </NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Company</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
              <li>
                <NavigationMenuLink href="#about">About</NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuIndicator />
      </NavigationMenuList>
    </NavigationMenu>
  );
}

Disabled

Disable a trigger so its flyout cannot open.

'use client';

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/nxds/components/navigation-menu/navigation-menu';

export default function NavigationMenuDisabled() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger disabled>Products</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
              <li>
                <NavigationMenuLink href="#analytics">
                  Analytics
                </NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  );
}

Props

NavigationMenu

NavigationMenu props
PropTypeDefaultDescription
viewportbooleantrue

Shows every flyout in one shared panel under the menu. Set false to show each flyout under its own trigger.

NavigationMenuContent

NavigationMenuContent adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuIndicator

NavigationMenuIndicator adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuItem

NavigationMenuItem adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuLink adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuList

NavigationMenuList adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuTrigger

NavigationMenuTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuViewport

NavigationMenuViewport adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenu shows top-level site links, with flyout panels for groups of pages. Use it for a website header. For app commands, use Menubar.

<script setup lang="ts">
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/navigation-menu';
</script>

<template>
  <NavigationMenu>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Products</NavigationMenuTrigger>
        <NavigationMenuContent>
          <ul class="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
            <li>
              <NavigationMenuLink href="#analytics"
                >Analytics</NavigationMenuLink
              >
            </li>
            <li>
              <NavigationMenuLink href="#automation"
                >Automation</NavigationMenuLink
              >
            </li>
            <li>
              <NavigationMenuLink href="#reports">Reports</NavigationMenuLink>
            </li>
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Company</NavigationMenuTrigger>
        <NavigationMenuContent>
          <ul class="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
            <li>
              <NavigationMenuLink href="#about">About</NavigationMenuLink>
            </li>
            <li>
              <NavigationMenuLink href="#careers">Careers</NavigationMenuLink>
            </li>
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenuList>
  </NavigationMenu>
</template>

Installation

  1. Install the following dependencies:

    npm install @phosphor-icons/vue@^2.2.1 @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0
    
  2. Copy and paste the following code into your project.

    components/navigation-menu/NavigationMenu.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      NavigationMenuRoot,
      type NavigationMenuRootEmits,
      type NavigationMenuRootProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import NavigationMenuViewport from './NavigationMenuViewport.vue';
    
    export interface NavigationMenuProps extends NavigationMenuRootProps {
      /**
       * Shows every flyout in one shared panel under the menu. Set `false` to
       * show each flyout under its own trigger.
       */
      viewport?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<NavigationMenuProps>(), {
      viewport: true,
    });
    const emits = defineEmits<NavigationMenuRootEmits>();
    
    const delegated = reactiveOmit(props, 'class', 'viewport');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <NavigationMenuRoot
        v-bind="forwarded"
        aria-label="Main"
        data-slot="navigation-menu"
        :data-viewport="viewport"
        :class="
          cn(
            'nx:group/navigation-menu nx:@container/navmenu nx:relative nx:flex nx:w-full nx:items-center nx:justify-center',
            props.class
          )
        "
      >
        <slot />
        <NavigationMenuViewport v-if="viewport" />
      </NavigationMenuRoot>
    </template>
    
    components/navigation-menu/NavigationMenuContent.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      NavigationMenuContent,
      type NavigationMenuContentEmits,
      type NavigationMenuContentProps as RekaNavigationMenuContentProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import {
      navigationMenuInlinePopoverSurfaceClassName,
      navigationMenuInlinePopoverTransitionClassName,
    } from '../overlay-layout/overlay-layout';
    
    export interface NavigationMenuContentProps extends RekaNavigationMenuContentProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<NavigationMenuContentProps>();
    const emits = defineEmits<NavigationMenuContentEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <NavigationMenuContent
        v-bind="forwarded"
        data-slot="navigation-menu-content"
        :class="
          cn(
            'nx:top-0 nx:left-0 nx:w-full nx:p-2 nx:pr-2.5',
            'nx:data-[motion=from-end]:slide-in-from-right-[1.5rem] nx:data-[motion=from-start]:slide-in-from-left-[1.5rem]',
            'nx:data-[motion=to-end]:slide-out-to-right-[1.5rem] nx:data-[motion=to-start]:slide-out-to-left-[1.5rem]',
            'nx:duration-fast nx:data-[motion^=from-]:animate-in nx:data-[motion^=from-]:fade-in nx:data-[motion^=to-]:animate-out nx:data-[motion^=to-]:fade-out',
            'nx:data-[motion^=from-]:duration-default nx:data-[motion^=from-]:ease-enter',
            'nx:data-[motion^=to-]:duration-fast nx:data-[motion^=to-]:ease-exit',
            'nx:@md/navmenu:absolute nx:@md/navmenu:w-auto',
            'nx:group-data-[viewport=false]/navigation-menu:top-full',
            'nx:group-data-[viewport=false]/navigation-menu:mt-1.5',
            'nx:group-data-[viewport=false]/navigation-menu:overflow-hidden',
            navigationMenuInlinePopoverSurfaceClassName,
            navigationMenuInlinePopoverTransitionClassName,
            props.class
          )
        "
      >
        <slot />
      </NavigationMenuContent>
    </template>
    
    components/navigation-menu/NavigationMenuIndicator.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      NavigationMenuIndicator,
      type NavigationMenuIndicatorProps as RekaNavigationMenuIndicatorProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface NavigationMenuIndicatorProps extends RekaNavigationMenuIndicatorProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<NavigationMenuIndicatorProps>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <NavigationMenuIndicator
        v-bind="forwarded"
        data-slot="navigation-menu-indicator"
        :class="
          cn(
            'nx:top-full nx:z-1 nx:flex nx:h-1.5 nx:items-end nx:justify-center nx:overflow-hidden',
            'nx:transition-opacity nx:duration-fast nx:ease-move',
            'nx:data-[state=hidden]:opacity-0 nx:data-[state=hidden]:animate-overlay-presence-exit',
            props.class
          )
        "
      >
        <div
          class="nx:relative nx:top-[60%] nx:size-2 nx:rotate-45 nx:rounded-tl-sm nx:bg-border-default nx:shadow-sm"
        />
      </NavigationMenuIndicator>
    </template>
    
    components/navigation-menu/NavigationMenuItem.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      NavigationMenuItem,
      type NavigationMenuItemProps as RekaNavigationMenuItemProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface NavigationMenuItemProps extends RekaNavigationMenuItemProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<NavigationMenuItemProps>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <NavigationMenuItem
        v-bind="forwarded"
        data-slot="navigation-menu-item"
        :class="cn('nx:relative', props.class)"
      >
        <slot />
      </NavigationMenuItem>
    </template>
    
    components/navigation-menu/NavigationMenuLink.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      NavigationMenuLink,
      type NavigationMenuLinkEmits,
      type NavigationMenuLinkProps as RekaNavigationMenuLinkProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface NavigationMenuLinkProps extends RekaNavigationMenuLinkProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<NavigationMenuLinkProps>();
    const emits = defineEmits<NavigationMenuLinkEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <NavigationMenuLink
        v-bind="forwarded"
        data-slot="navigation-menu-link"
        :class="
          cn(
            'nx:flex nx:flex-col nx:gap-1 nx:rounded-sm nx:p-2 nx:text-inherit nx:no-underline nx:typography-body-default nx:transition-control',
            'nx:hover:bg-popover-hover nx:hover:text-popover-foreground',
            'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default',
            'nx:data-[active=true]:bg-popover-hover nx:data-[active=true]:text-popover-foreground',
            'nx:[&_svg:not([class*=size-])]:size-4 nx:[&_svg:not([class*=text-])]:text-muted-foreground',
            props.class
          )
        "
      >
        <slot />
      </NavigationMenuLink>
    </template>
    
    components/navigation-menu/NavigationMenuList.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      NavigationMenuList,
      type NavigationMenuListProps as RekaNavigationMenuListProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface NavigationMenuListProps extends RekaNavigationMenuListProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<NavigationMenuListProps>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <NavigationMenuList
        v-bind="forwarded"
        data-slot="navigation-menu-list"
        :class="
          cn(
            'nx:group nx:m-0 nx:flex nx:flex-1 nx:list-none nx:items-center nx:justify-center nx:gap-1 nx:p-0',
            props.class
          )
        "
      >
        <slot />
      </NavigationMenuList>
    </template>
    
    components/navigation-menu/NavigationMenuTrigger.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      NavigationMenuTrigger,
      type NavigationMenuTriggerProps as RekaNavigationMenuTriggerProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { IconChevronDown } from '../../lib/icons';
    import { cn } from '../../lib/utils';
    
    import { navigationMenuTriggerStyle } from '.';
    
    export interface NavigationMenuTriggerProps extends RekaNavigationMenuTriggerProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<NavigationMenuTriggerProps>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <NavigationMenuTrigger
        v-bind="forwarded"
        data-slot="navigation-menu-trigger"
        :class="cn(navigationMenuTriggerStyle(), 'nx:group', props.class)"
      >
        <slot />{{ ' ' }}
        <IconChevronDown
          class="nx:relative nx:top-px nx:ml-1 nx:size-3 nx:transition nx:duration-slow nx:group-data-[state=open]:rotate-180"
          aria-hidden="true"
        />
      </NavigationMenuTrigger>
    </template>
    
    components/navigation-menu/NavigationMenuViewport.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      NavigationMenuViewport,
      type NavigationMenuViewportProps as RekaNavigationMenuViewportProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import {
      overlayFloatingTransitionClassName,
      popoverSurfaceClassName,
    } from '../overlay-layout/overlay-layout';
    
    export interface NavigationMenuViewportProps extends RekaNavigationMenuViewportProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<NavigationMenuViewportProps>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <div
        class="nx:absolute nx:top-full nx:left-0 nx:isolate nx:z-popover nx:flex nx:justify-center"
      >
        <NavigationMenuViewport
          v-bind="forwarded"
          data-slot="navigation-menu-viewport"
          :class="
            cn(
              'nx:origin-top nx:relative nx:mt-1.5 nx:h-(--reka-navigation-menu-viewport-height) nx:w-full nx:overflow-hidden',
              popoverSurfaceClassName,
              overlayFloatingTransitionClassName,
              'nx:@md/navmenu:w-(--reka-navigation-menu-viewport-width)',
              props.class
            )
          "
        />
      </div>
    </template>
    
    components/navigation-menu/index.ts
    import { cva } from 'class-variance-authority';
    
    export {
      default as NavigationMenu,
      type NavigationMenuProps,
    } from './NavigationMenu.vue';
    export {
      default as NavigationMenuContent,
      type NavigationMenuContentProps,
    } from './NavigationMenuContent.vue';
    export {
      default as NavigationMenuIndicator,
      type NavigationMenuIndicatorProps,
    } from './NavigationMenuIndicator.vue';
    export {
      default as NavigationMenuItem,
      type NavigationMenuItemProps,
    } from './NavigationMenuItem.vue';
    export {
      default as NavigationMenuLink,
      type NavigationMenuLinkProps,
    } from './NavigationMenuLink.vue';
    export {
      default as NavigationMenuList,
      type NavigationMenuListProps,
    } from './NavigationMenuList.vue';
    export {
      default as NavigationMenuTrigger,
      type NavigationMenuTriggerProps,
    } from './NavigationMenuTrigger.vue';
    export {
      default as NavigationMenuViewport,
      type NavigationMenuViewportProps,
    } from './NavigationMenuViewport.vue';
    
    export const navigationMenuTriggerStyle = cva(
      'nx:group nx:inline-flex nx:w-max nx:items-center nx:justify-center nx:rounded-md nx:border-0 nx:bg-background nx:px-4 nx:py-2 nx:text-inherit nx:typography-label-default nx:transition-control nx:hover:bg-background-hover nx:hover:text-foreground nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default nx:disabled:pointer-events-none nx:disabled:text-disabled-foreground nx:data-[state=open]:bg-background-hover nx:data-[state=open]:text-foreground'
    );
    
    components/overlay-layout/overlay-layout.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    /**
     * Popover-family surfaces share the translucent/blurred recipe; Tooltip uses a
     * solid carve-out because its small, brief text appears over arbitrary content.
     */
    export const popoverSurfaceClassName = [
      'nx:rounded-md',
      'nx:border-default',
      'nx:border-border-default',
      'nx:bg-popover-alpha',
      'nx:backdrop-blur-md',
      'nx:text-popover-foreground',
      'nx:typography-body-default',
      'nx:shadow-lg',
    ].join(' ');
    
    export const tooltipSurfaceClassName = [
      'nx:rounded-md',
      'nx:border-default',
      'nx:border-border-default',
      'nx:bg-popover',
      'nx:text-popover-foreground',
      'nx:typography-body-default',
      'nx:shadow-lg',
    ].join(' ');
    
    export const navigationMenuInlinePopoverSurfaceClassName = [
      'nx:group-data-[viewport=false]/navigation-menu:rounded-md',
      'nx:group-data-[viewport=false]/navigation-menu:border-default',
      'nx:group-data-[viewport=false]/navigation-menu:border-border-default',
      'nx:group-data-[viewport=false]/navigation-menu:bg-popover-alpha',
      'nx:group-data-[viewport=false]/navigation-menu:backdrop-blur-md',
      'nx:group-data-[viewport=false]/navigation-menu:text-popover-foreground',
      'nx:group-data-[viewport=false]/navigation-menu:shadow-lg',
    ].join(' ');
    
    export const navigationMenuInlinePopoverTransitionClassName = [
      'nx:group-data-[viewport=false]/navigation-menu:transition-[opacity,scale]',
      'nx:group-data-[viewport=false]/navigation-menu:duration-fast',
      'nx:group-data-[viewport=false]/navigation-menu:ease-move',
      'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:scale-95',
      'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:opacity-0',
      'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-overlay-presence-exit',
    ].join(' ');
    
    // `animate-overlay-presence-exit` is a non-visual "presence bridge": Reka's
    // Presence only waits on `animationName`, not `transitionend`, so this inert
    // animation keeps a closing overlay mounted while the opacity/scale/translate
    // TRANSITIONS below run the visible exit. It animates an unread custom property (see
    // generateMotionUtilitiesCSS in @nxds/core), never a transitioned property, so it
    // cannot override those transitions.
    const overlayPresenceExitClassName =
      'nx:data-[state=closed]:animate-overlay-presence-exit';
    
    export const overlayFloatingTransitionClassName = [
      'nx:transition-[opacity,scale]',
      'nx:duration-fast',
      'nx:ease-move',
      'nx:data-[state=closed]:scale-95',
      'nx:data-[state=closed]:opacity-0',
      overlayPresenceExitClassName,
    ].join(' ');
    
    const overlayContentTransitionClassName = [
      'nx:transition-[opacity,scale]',
      'nx:duration-default',
      'nx:ease-move',
      'nx:data-[state=closed]:scale-95',
      'nx:data-[state=closed]:opacity-0',
      overlayPresenceExitClassName,
    ].join(' ');
    
    export const overlayScrimTransitionClassName = [
      'nx:transition-opacity',
      'nx:duration-default',
      'nx:ease-move',
      'nx:data-[state=closed]:opacity-0',
      overlayPresenceExitClassName,
    ].join(' ');
    
    export const overlayPanelTransitionClassName = [
      'nx:transition-[translate]',
      'nx:duration-slow',
      'nx:ease-move',
      overlayPresenceExitClassName,
    ].join(' ');
    
    export const overlayContentVariants = cva(
      [
        // Centred with margins, not a translate: a transform would make this the
        // containing block for fixed-position popups a dialog holds.
        'nx:fixed nx:inset-0 nx:z-modal nx:m-auto nx:flex nx:h-fit nx:w-full nx:max-w-lg nx:flex-col',
        'nx:max-h-[calc(100svh-2rem)] nx:overflow-hidden',
        'nx:gap-4 nx:border-default nx:border-border-default nx:bg-container nx:py-6 nx:shadow-lg',
        overlayContentTransitionClassName,
        'nx:sm:rounded-lg',
      ].join(' ')
    );
    
    export const overlayScrimVariants = cva(
      [
        'nx:fixed nx:inset-0 nx:z-modal nx:bg-overlay',
        overlayScrimTransitionClassName,
      ].join(' ')
    );
    
    export const overlayHeaderVariants = cva(
      'nx:flex nx:shrink-0 nx:flex-col nx:gap-1 nx:px-6',
      {
        variants: {
          variant: {
            default: 'nx:text-center nx:sm:text-left',
            center: 'nx:items-center nx:text-center',
          },
        },
        defaultVariants: {
          variant: 'default',
        },
      }
    );
    
    export const overlayBodyClassName = [
      'nx:min-h-0',
      'nx:overflow-y-auto',
      'nx:px-6',
      'nx:focus-visible:outline-2',
      'nx:focus-visible:outline-focus-default',
      'nx:focus-visible:[outline-offset:-2px]',
    ].join(' ');
    
    export const overlayFooterVariants = cva(
      'nx:flex nx:shrink-0 nx:gap-2 nx:px-6',
      {
        variants: {
          orientation: {
            horizontal:
              'nx:flex-col nx:sm:flex-row nx:sm:items-center nx:sm:justify-end',
            vertical: 'nx:flex-col nx:items-stretch nx:*:w-full',
          },
        },
        defaultVariants: {
          orientation: 'horizontal',
        },
      }
    );
    
    export const overlayCloseButtonClassName = [
      'nx:absolute nx:right-6 nx:top-6 nx:rounded-sm nx:border-0 nx:bg-transparent nx:p-1 nx:text-muted-foreground-subtle',
      'nx:transition-control',
      'nx:hover:bg-container-hover nx:hover:text-foreground',
      'nx:focus-visible:bg-container-hover nx:focus-visible:text-foreground',
      'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default',
      'nx:disabled:pointer-events-none',
    ].join(' ');
    
    export type OverlayHeaderVariants = VariantProps<typeof overlayHeaderVariants>;
    
    export type OverlayVariant = NonNullable<
      VariantProps<typeof overlayHeaderVariants>['variant']
    >;
    
    export type OverlayButtonOrientation = NonNullable<
      VariantProps<typeof overlayFooterVariants>['orientation']
    >;
    
    export type OverlayLayoutContextValue = {
      variant: OverlayVariant;
      buttonOrientation: OverlayButtonOrientation;
    };
    
    export const defaultOverlayLayout: OverlayLayoutContextValue = {
      variant: 'default',
      buttonOrientation: 'horizontal',
    };
    
    export function resolveOverlayButtonOrientation(
      variant: OverlayVariant
    ): OverlayButtonOrientation {
      return variant === 'center' ? 'vertical' : 'horizontal';
    }
    
    lib/icons.ts
    /**
     * Internal Icons
     *
     * This barrel file re-exports icons used internally by the design system.
     * These are hardcoded icons for component states (loading, validation, navigation).
     *
     * User-facing icons should be passed via props by consumers.
     *
     * This set draws Phosphor. `nxds init --icons` installs the same names drawn
     * by another library.
     */
    import { PhCircle } from '@phosphor-icons/vue';
    import { type FunctionalComponent, h } from 'vue';
    
    // Loading states
    export { PhCircleNotch as IconLoader2 } from '@phosphor-icons/vue';
    
    // Navigation
    export {
      PhArrowDown as IconArrowDown,
      PhArrowLeft as IconArrowLeft,
      PhArrowRight as IconArrowRight,
      PhArrowUp as IconArrowUp,
      PhCaretDown as IconChevronDown,
      PhCaretLeft as IconChevronLeft,
      PhCaretRight as IconChevronRight,
      PhCaretUp as IconChevronUp,
      PhDotsThree as IconDots,
      PhSidebarSimple as IconLayoutSidebar,
      PhCaretUpDown as IconSelector,
    } from '@phosphor-icons/vue';
    
    // Actions
    export {
      PhCheck as IconCheck,
      PhCopy as IconCopy,
      PhMinus as IconMinus,
      PhPencilSimple as IconPencil,
      PhPlus as IconPlus,
      PhX as IconX,
    } from '@phosphor-icons/vue';
    
    export const IconCircleFilled: FunctionalComponent = (_props, { attrs }) =>
      h(PhCircle, { weight: 'fill', ...attrs });
    
    // Filter field types
    export {
      PhCalendarBlank as IconCalendar,
      PhHash as IconHash,
      PhTextAa as IconLetterCase,
      PhListBullets as IconList,
    } from '@phosphor-icons/vue';
    
    // Search / input
    export { PhMagnifyingGlass as IconSearch } from '@phosphor-icons/vue';
    
    // Alerts/Status
    export {
      PhWarningCircle as IconAlertCircle,
      PhWarning as IconAlertTriangle,
      PhCheckCircle as IconCircleCheck,
      PhInfo as IconInfoCircle,
    } from '@phosphor-icons/vue';
    
    // Layout
    export {
      PhDotsSixVertical as IconGripVertical,
      PhPalette as IconPalette,
    } from '@phosphor-icons/vue';
    
  3. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle } from '@/components/navigation-menu';

Examples

Style a plain link with navigationMenuTriggerStyle() to sit beside triggers.

<script setup lang="ts">
import {
  NavigationMenu,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  navigationMenuTriggerStyle,
} from '@/components/navigation-menu';

const triggerStyle = navigationMenuTriggerStyle();
</script>

<template>
  <NavigationMenu>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuLink href="#docs" :class="triggerStyle"
          >Docs</NavigationMenuLink
        >
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink href="#pricing" :class="triggerStyle"
          >Pricing</NavigationMenuLink
        >
      </NavigationMenuItem>
    </NavigationMenuList>
  </NavigationMenu>
</template>

Without viewport

Set :viewport="false" to open each flyout under its own trigger.

<script setup lang="ts">
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/navigation-menu';
</script>

<template>
  <NavigationMenu :viewport="false">
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Products</NavigationMenuTrigger>
        <NavigationMenuContent>
          <ul class="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
            <li>
              <NavigationMenuLink href="#analytics"
                >Analytics</NavigationMenuLink
              >
            </li>
            <li>
              <NavigationMenuLink href="#reports">Reports</NavigationMenuLink>
            </li>
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenuList>
  </NavigationMenu>
</template>

With indicator

Add NavigationMenuIndicator to point an arrow at the open flyout.

<script setup lang="ts">
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuIndicator,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/navigation-menu';
</script>

<template>
  <NavigationMenu>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Products</NavigationMenuTrigger>
        <NavigationMenuContent>
          <ul class="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
            <li>
              <NavigationMenuLink href="#analytics"
                >Analytics</NavigationMenuLink
              >
            </li>
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Company</NavigationMenuTrigger>
        <NavigationMenuContent>
          <ul class="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
            <li>
              <NavigationMenuLink href="#about">About</NavigationMenuLink>
            </li>
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuIndicator />
    </NavigationMenuList>
  </NavigationMenu>
</template>

Disabled

Disable a trigger so its flyout cannot open.

<script setup lang="ts">
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/navigation-menu';
</script>

<template>
  <NavigationMenu>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger disabled>Products</NavigationMenuTrigger>
        <NavigationMenuContent>
          <ul class="nx:grid nx:w-[320px] nx:gap-1 nx:p-2">
            <li>
              <NavigationMenuLink href="#analytics"
                >Analytics</NavigationMenuLink
              >
            </li>
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenuList>
  </NavigationMenu>
</template>

Props

NavigationMenu

NavigationMenu props
PropTypeDefaultDescription
viewportbooleantrue

Shows every flyout in one shared panel under the menu. Set false to show each flyout under its own trigger.

NavigationMenuContent

NavigationMenuContent adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuIndicator

NavigationMenuIndicator adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuItem

NavigationMenuItem adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuLink adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuList

NavigationMenuList adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuTrigger

NavigationMenuTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.

NavigationMenuViewport

NavigationMenuViewport adds no props of its own; it accepts the props of the element or primitive it wraps.