# Navigation Menu

A collection of links for navigating websites.

```tsx
import type { ReactNode } from "react"
import * as stylex from "@stylexjs/stylex"
import { CircleAlert, CircleCheck, CircleDashed } from "lucide-react"
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuStyles,
} from "@/components/ui/navigation-menu"
import { VStack } from "@/components/ui/stack"
import { breakpoints } from "@/theme/breakpoints.stylex"
import { color, space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  list: { width: "24rem" },
  grid: {
    display: "grid",
    gap: space.s2,
    width: { default: "25rem", [breakpoints.md]: "31.25rem", [breakpoints.lg]: "37.5rem" },
    gridTemplateColumns: { default: "1fr", [breakpoints.md]: "repeat(2, 1fr)" },
  },
  narrow: { width: "12.5rem" },
  wideOnly: { display: { default: "none", [breakpoints.md]: "flex" } },
  title: { fontWeight: 500, lineHeight: 1 },
  description: {
    display: "-webkit-box",
    WebkitLineClamp: 2,
    WebkitBoxOrient: "vertical",
    overflow: "hidden",
    color: color.mutedForeground,
  },
})

const components: { title: string; href: string; description: string }[] = [
  {
    title: "Alert Dialog",
    href: "/docs/components/alert-dialog",
    description:
      "A modal dialog that interrupts the user with important content and expects a response.",
  },
  {
    title: "Hover Card",
    href: "/docs/components/hover-card",
    description: "For sighted users to preview content available behind a link.",
  },
  {
    title: "Progress",
    href: "/docs/components/progress",
    description:
      "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.",
  },
  {
    title: "Scroll Area",
    href: "/docs/components/scroll-area",
    description: "Visually or semantically separates content.",
  },
  {
    title: "Tabs",
    href: "/docs/components/tabs",
    description:
      "A set of layered sections of content, known as tab panels, that are displayed one at a time.",
  },
  {
    title: "Tooltip",
    href: "/docs/components/tooltip",
    description:
      "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
  },
]

export default function NavigationMenuDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul {...stylex.props(styles.list)}>
              <ListItem href="/docs/installation" title="Introduction">
                Re-usable components built with Base UI and StyleX.
              </ListItem>
              <ListItem href="/docs/installation" title="Installation">
                How to install dependencies and structure your app.
              </ListItem>
              <ListItem href="/docs/components/stack" title="Stack">
                Direction, gap, padding and alignment on one closed scale.
              </ListItem>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem xstyle={styles.wideOnly}>
          <NavigationMenuTrigger>Components</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul {...stylex.props(styles.grid)}>
              {components.map((component) => (
                <ListItem key={component.title} title={component.title} href={component.href}>
                  {component.description}
                </ListItem>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuTrigger>With Icon</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul {...stylex.props(styles.narrow)}>
              <li>
                <NavigationMenuLink href="#">
                  <CircleAlert />
                  Backlog
                </NavigationMenuLink>
                <NavigationMenuLink href="#">
                  <CircleDashed />
                  To Do
                </NavigationMenuLink>
                <NavigationMenuLink href="#">
                  <CircleCheck />
                  Done
                </NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink href="/docs/installation" xstyle={navigationMenuStyles.trigger}>
            Docs
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

function ListItem({ title, children, href }: { title: string; children: ReactNode; href: string }) {
  return (
    <li>
      <NavigationMenuLink href={href}>
        <VStack gap={1}>
          <span {...stylex.props(styles.title)}>{title}</span>
          <span {...stylex.props(styles.description)}>{children}</span>
        </VStack>
      </NavigationMenuLink>
    </li>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx lucide-react
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and `state-styles.ts`
   from the same page. `states.ts` carries the popup's surface, the positioner's
   layer and the focus ring that a trigger and a link share. `state-styles.ts` is
   how every part reads its Base UI state, such as the open trigger, the sliding
   panel and the active link. The menu also needs `interop.css` to size the icons a link
   carries.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/navigation-menu.tsx"
   "use client"

   import { createContext, use } from "react"
   import type { CSSProperties } from "react"
   import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"
   import { ChevronDown } from "lucide-react"

   import { stateStyles } from "@/lib/state-styles"
   import { states } from "@/theme/states"
   import {
     color,
     duration,
     ease,
     leading,
     radius,
     shadow,
     space,
     text,
   } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     root: {
       position: "relative",
       display: "flex",
       maxWidth: "max-content",
       alignItems: "center",
       justifyContent: "center",
     },
     list: {
       display: "flex",
       alignItems: "center",
       justifyContent: "center",
       gap: space.s1,
     },
     listVertical: { flexDirection: "column", alignItems: "stretch" },
     item: { position: "relative" },
     trigger: {
       display: "inline-flex",
       alignItems: "center",
       justifyContent: "center",
       gap: space.s1,
       height: space.s8,
       width: "max-content",
       borderRadius: radius.lg,
       paddingInline: space.s2_5,
       paddingBlock: space.s1_5,
       backgroundColor: {
         default: `var(--carbon-nav-current-bg, ${color.transparent})`,
         ":hover": color.itemHighlightBg,
       },
       fontSize: text.sm,
       lineHeight: leading.sm,
       fontWeight: 500,
       transitionProperty: "color, background-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       pointerEvents: { default: null, ":disabled": "none" },
       opacity: { default: 1, ":disabled": 0.5 },
       "--carbon-icon-size": space.s4,
     },
     surface: {
       boxShadow: {
         default: `var(--carbon-nav-current-ring, inset 0 0 0 0 ${color.transparent})`,
         ":focus-visible": `var(--carbon-nav-current-ring, inset 0 0 0 0 ${color.transparent}), ${shadow.ring}`,
       },
     },
     current: {
       "--carbon-nav-current-bg": color.itemHighlightBg,
       "--carbon-nav-current-ring": `inset 0 0 0 1px ${color.itemHighlightRing}`,
     },
     chevron: {
       display: "inline-flex",
       position: "relative",
       top: 1,
       "--carbon-icon-size": space.s3,
       transitionProperty: "transform",
       transitionDuration: { default: duration.move, "@media (prefers-reduced-motion: reduce)": "0s" },
       transitionTimingFunction: ease.standard,
     },
     chevronOpen: { transform: "rotate(180deg)" },
     link: {
       display: "flex",
       alignItems: "center",
       gap: space.s2,
       borderRadius: radius.lg,
       paddingInline: space.s2,
       paddingBlock: space.s2,
       backgroundColor: {
         default: `var(--carbon-nav-current-bg, ${color.transparent})`,
         ":hover": color.itemHighlightBg,
       },
       fontSize: text.sm,
       lineHeight: leading.sm,
       transitionProperty: "color, background-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       "--carbon-icon-size": space.s4,
     },
     content: {
       width: "auto",
       height: "100%",
       padding: space.s1,
       transitionProperty: "opacity, transform",
       transitionDuration: {
         default: `150ms, ${duration.panel}`,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: `${ease.standard}, ${ease.decelerate}`,
     },
     hidden: { opacity: 0 },
     positioner: {
       width: "var(--positioner-width)",
       height: "var(--positioner-height)",
       maxWidth: "var(--available-width)",
       transitionProperty: "top, left, right, bottom",
       transitionDuration: {
         default: duration.panel,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.decelerate,
       "::before": { content: "''", position: "absolute" },
     },
     positionerInstant: { transitionDuration: "0s" },
     popup: {
       position: "relative",
       width: "var(--popup-width)",
       height: "var(--popup-height)",
       "--carbon-popup-radius": radius.lg,
       borderRadius: "var(--carbon-popup-radius)",
       transformOrigin: "var(--transform-origin)",
       transitionProperty: "opacity, transform, width, height",
       transitionDuration: {
         default: duration.panel,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.decelerate,
     },
     popupHidden: { opacity: 0, transform: "scale(0.95)" },
     popupEnding: {
       transitionDuration: { default: "150ms", "@media (prefers-reduced-motion: reduce)": "0s" },
       transitionTimingFunction: ease.standard,
     },
     viewport: { position: "relative", width: "100%", height: "100%", overflow: "hidden" },
   })

   const bridge = stylex.create({
     bottom: {
       "::before": { top: `calc(-1 * ${space.s2_5})`, right: 0, left: 0, height: space.s2_5 },
     },
     top: {
       "::before": { bottom: `calc(-1 * ${space.s2_5})`, right: 0, left: 0, height: space.s2_5 },
     },
     left: {
       "::before": { top: 0, bottom: 0, right: `calc(-1 * ${space.s2_5})`, width: space.s2_5 },
     },
     right: {
       "::before": { top: 0, bottom: 0, left: `calc(-1 * ${space.s2_5})`, width: space.s2_5 },
     },
     "inline-start": {
       "::before": { top: 0, bottom: 0, right: `calc(-1 * ${space.s2_5})`, width: space.s2_5 },
     },
     "inline-end": {
       "::before": { top: 0, bottom: 0, left: `calc(-1 * ${space.s2_5})`, width: space.s2_5 },
     },
   })

   const slide = stylex.create({
     left: { transform: "translateX(-50%)" },
     right: { transform: "translateX(50%)" },
     up: { transform: "translateY(-50%)" },
     down: { transform: "translateY(50%)" },
   })

   const opposite = { left: "right", right: "left", up: "down", down: "up" } as const

   type Orientation = "horizontal" | "vertical"

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type StateOverrides = Overrides & { style?: CSSProperties }

   const NavigationMenuOrientationContext = createContext<Orientation>("horizontal")

   const NavigationMenuContentContext = createContext(false)

   type NavigationMenuPositionerProps = Omit<
     NavigationMenuPrimitive.Positioner.Props,
     "className" | "style"
   > & {
     style?: CSSProperties
     popupProps?: Omit<NavigationMenuPrimitive.Popup.Props, "className" | "style"> & StateOverrides
   }

   function NavigationMenuPositioner({
     side = "bottom",
     sideOffset = 8,
     align = "start",
     alignOffset = 0,
     popupProps,
     style,
     ...props
   }: NavigationMenuPositionerProps) {
     const { xstyle, className, style: popupStyle, ...popup } = popupProps ?? {}

     return (
       <NavigationMenuPrimitive.Portal>
         <NavigationMenuPrimitive.Positioner
           data-slot="navigation-menu-positioner"
           side={side}
           sideOffset={sideOffset}
           align={align}
           alignOffset={alignOffset}
           {...props}
           {...stateStyles(
             (state) => [
               states.positioner,
               styles.positioner,
               bridge[state.side],
               state.instant && styles.positionerInstant,
             ],
             { style },
           )}
         >
           <NavigationMenuPrimitive.Popup
             data-slot="navigation-menu-popup"
             {...popup}
             {...stateStyles(
               (state) => [
                 states.popupSurface,
                 styles.popup,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   styles.popupHidden,
                 state.transitionStatus === "ending" && styles.popupEnding,
                 xstyle,
               ],
               { className, style: popupStyle },
             )}
           >
             <NavigationMenuPrimitive.Viewport
               data-slot="navigation-menu-viewport"
               {...stylex.props(styles.viewport)}
             />
           </NavigationMenuPrimitive.Popup>
         </NavigationMenuPrimitive.Positioner>
       </NavigationMenuPrimitive.Portal>
     )
   }

   type NavigationMenuProps<Value> = Omit<NavigationMenuPrimitive.Root.Props<Value>, "className"> &
     Pick<
       NavigationMenuPositionerProps,
       "side" | "sideOffset" | "align" | "alignOffset" | "popupProps"
     > &
     Overrides

   function NavigationMenu<Value = any>({
     orientation = "horizontal",
     side = orientation === "vertical" ? "right" : "bottom",
     sideOffset,
     align,
     alignOffset,
     popupProps,
     xstyle,
     className,
     children,
     ...props
   }: NavigationMenuProps<Value>) {
     const sx = stylex.props(styles.root, xstyle)

     return (
       <NavigationMenuOrientationContext value={orientation}>
         <NavigationMenuPrimitive.Root
           data-slot="navigation-menu"
           data-orientation={orientation}
           orientation={orientation}
           {...sx}
           {...props}
           className={clsx(sx.className, className)}
         >
           {children}
           <NavigationMenuPositioner
             side={side}
             sideOffset={sideOffset}
             align={align}
             alignOffset={alignOffset}
             popupProps={popupProps}
           />
         </NavigationMenuPrimitive.Root>
       </NavigationMenuOrientationContext>
     )
   }

   type NavigationMenuListProps = Omit<NavigationMenuPrimitive.List.Props, "className"> & Overrides

   function NavigationMenuList({ xstyle, className, ...props }: NavigationMenuListProps) {
     const orientation = use(NavigationMenuOrientationContext)
     const sx = stylex.props(styles.list, orientation === "vertical" && styles.listVertical, xstyle)

     return (
       <NavigationMenuPrimitive.List
         data-slot="navigation-menu-list"
         data-orientation={orientation}
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type NavigationMenuItemProps = Omit<NavigationMenuPrimitive.Item.Props, "className"> & Overrides

   function NavigationMenuItem({ xstyle, className, ...props }: NavigationMenuItemProps) {
     const sx = stylex.props(styles.item, xstyle)

     return (
       <NavigationMenuPrimitive.Item
         data-slot="navigation-menu-item"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type NavigationMenuTriggerProps = Omit<
     NavigationMenuPrimitive.Trigger.Props,
     "className" | "style"
   > &
     StateOverrides

   function NavigationMenuTrigger({
     xstyle,
     style,
     className,
     children,
     ...props
   }: NavigationMenuTriggerProps) {
     return (
       <NavigationMenuPrimitive.Trigger
         data-slot="navigation-menu-trigger"
         {...props}
         {...stateStyles(
           (state) => [
             styles.trigger,
             states.focusRing,
             styles.surface,
             state.open && styles.current,
             xstyle,
           ],
           { className, style, hook: "carbon-icons" },
         )}
       >
         {children}
         <NavigationMenuPrimitive.Icon
           data-slot="navigation-menu-trigger-icon"
           {...stateStyles((state) => [styles.chevron, state.open && styles.chevronOpen])}
         >
           <ChevronDown aria-hidden="true" />
         </NavigationMenuPrimitive.Icon>
       </NavigationMenuPrimitive.Trigger>
     )
   }

   type NavigationMenuContentProps = Omit<
     NavigationMenuPrimitive.Content.Props,
     "className" | "style"
   > &
     StateOverrides

   function NavigationMenuContent({ xstyle, style, className, ...props }: NavigationMenuContentProps) {
     return (
       <NavigationMenuContentContext value={true}>
         <NavigationMenuPrimitive.Content
           data-slot="navigation-menu-content"
           {...props}
           {...stateStyles(
             (state) => {
               const direction = state.activationDirection
               const entering = state.transitionStatus === "starting"
               const leaving = state.transitionStatus === "ending"

               return [
                 styles.content,
                 (entering || leaving) && styles.hidden,
                 direction && entering && slide[direction],
                 direction && leaving && slide[opposite[direction]],
                 xstyle,
               ]
             },
             { className, style },
           )}
         />
       </NavigationMenuContentContext>
     )
   }

   type NavigationMenuLinkProps = Omit<NavigationMenuPrimitive.Link.Props, "className" | "style"> &
     StateOverrides

   function NavigationMenuLink({ xstyle, style, className, ...props }: NavigationMenuLinkProps) {
     const inContent = use(NavigationMenuContentContext)

     return (
       <NavigationMenuPrimitive.Link
         data-slot="navigation-menu-link"
         {...props}
         {...stateStyles(
           (state) => [
             styles.link,
             states.focusRing,
             styles.surface,
             inContent && states.popupItem,
             state.active && styles.current,
             xstyle,
           ],
           { className, style, hook: "carbon-icons" },
         )}
       />
     )
   }

   const navigationMenuStyles = {
     trigger: [styles.trigger, states.focusRing, styles.surface],
     link: [styles.link, states.focusRing, styles.surface],
   }

   export {
     NavigationMenu,
     NavigationMenuContent,
     NavigationMenuItem,
     NavigationMenuLink,
     NavigationMenuList,
     NavigationMenuPositioner,
     NavigationMenuTrigger,
     navigationMenuStyles,
   }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"
```

```tsx
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Item One</NavigationMenuTrigger>
      <NavigationMenuContent>
        <NavigationMenuLink href="/one">Link</NavigationMenuLink>
      </NavigationMenuContent>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>
```

## Composition

```text
NavigationMenu                      nav + portal, positioner, popup, viewport
└── NavigationMenuList
    ├── NavigationMenuItem
    │   ├── NavigationMenuTrigger
    │   └── NavigationMenuContent
    │       ├── NavigationMenuLink
    │       └── NavigationMenuLink
    └── NavigationMenuItem
        └── NavigationMenuLink
```

There is one popup for the whole menu. `NavigationMenu` renders it, and every
`NavigationMenuContent` is shown inside it in turn, so the panel resizes and
slides between triggers instead of closing and reopening.

There is no `NavigationMenuIndicator`. shadcn's is a Radix arrow rendered
through Base UI's `Icon` part, which is the trigger's chevron here.

## Link

Use the `render` prop to hand navigation to a router link. `href` alone renders
a plain `<a>`.

```tsx
import { Link } from "@tanstack/react-router"

import {
  NavigationMenuItem,
  NavigationMenuLink,
  navigationMenuStyles,
} from "@/components/ui/navigation-menu"

export function DocsLink() {
  return (
    <NavigationMenuItem>
      <NavigationMenuLink render={<Link to="/docs" />} xstyle={navigationMenuStyles.trigger}>
        Documentation
      </NavigationMenuLink>
    </NavigationMenuItem>
  )
}
```

`navigationMenuStyles.trigger` gives a top-level link the trigger's height and
padding so it lines up with its neighbours. Pass `active` to mark the current
page. The link then takes the open trigger's fill and `aria-current="page"`.

## Large menus

The popup grows to fit the panel it shows. Cap it with `popupProps` when a
panel can be taller than the viewport, and cap the panel to match.

```tsx
const styles = stylex.create({
  capped: { maxHeight: "var(--available-height)" },
  scrolls: { maxHeight: "var(--available-height)", overflowY: "auto" },
})

<NavigationMenu popupProps={{ xstyle: styles.capped }}>
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Products</NavigationMenuTrigger>
      <NavigationMenuContent xstyle={styles.scrolls}>{/* … */}</NavigationMenuContent>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>
```

> **Note**
>
> A native scrollbar stays visible while the panel slides. If that is a problem,
> put a [Scroll Area](https://carbon.style/docs/components/scroll-area) inside the content instead.

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `NavigationMenu` part of the same name and accepts
its props. See the
[Base UI reference](https://base-ui.com/react/components/navigation-menu) for
the full list. Each part also takes `xstyle` and `className`.
`NavigationMenuTrigger`, `NavigationMenuContent`, `NavigationMenuLink` and the
popup reached through `popupProps` take `style` too. It is real inline CSS,
merged over whatever their Base UI state resolves to, not a StyleX override.

### NavigationMenu

Renders a `<nav>` and, after its children, the portal, positioner, popup and
viewport. The positioning props are forwarded to the positioner.

| Prop            | Type                                                                 | Default                             |
| --------------- | -------------------------------------------------------------------- | ----------------------------------- |
| `value`         | `Value \| null`                                                      | —                                   |
| `defaultValue`  | `Value \| null`                                                      | `null`                              |
| `onValueChange` | `(value: Value \| null, details) => void`                            | —                                   |
| `delay`         | `number`                                                             | `50`                                |
| `closeDelay`    | `number`                                                             | `50`                                |
| `orientation`   | `"horizontal" \| "vertical"`                                         | `"horizontal"`                      |
| `side`          | `"top" \| "bottom" \| "left" \| "right"`                             | `"bottom"`, `"right"` when vertical |
| `sideOffset`    | `number`                                                             | `8`                                 |
| `align`         | `"start" \| "center" \| "end"`                                       | `"start"`                           |
| `alignOffset`   | `number`                                                             | `0`                                 |
| `popupProps`    | `NavigationMenuPrimitive.Popup.Props & { xstyle, className, style }` | —                                   |

### NavigationMenuLink

| Prop           | Type      | Default |
| -------------- | --------- | ------- |
| `active`       | `boolean` | `false` |
| `closeOnClick` | `boolean` | `true`  |

### NavigationMenuContent

| Prop          | Type      | Default |
| ------------- | --------- | ------- |
| `keepMounted` | `boolean` | `false` |

### navigationMenuStyles

| Key       | Description                                                                   |
| --------- | ----------------------------------------------------------------------------- |
| `trigger` | The trigger's box and the shared focus ring, for a link that sits beside them |
| `link`    | The link's box and the same ring, for a control in a panel that is not a link |

Each key pairs the part's own box with `states.focusRing` from
`theme/states.ts`, so an element that uses one shows the same focus ring as the
part it came from. The popup's fill and hairline ring are `states.popupSurface`
and the positioner's layer is `states.positioner`. Neither is in the map,
because neither is a box you would put on an element of your own.

### Data attributes

| Attribute                                  | On              | Effect                                                 |
| ------------------------------------------ | --------------- | ------------------------------------------------------ |
| `data-slot="navigation-menu"`              | the `nav`       |                                                        |
| `data-orientation`                         | the `nav`, list | Mirrors `orientation`                                  |
| `data-slot="navigation-menu-list"`         | the `ul`        |                                                        |
| `data-slot="navigation-menu-item"`         | each `li`       |                                                        |
| `data-slot="navigation-menu-trigger"`      | a trigger       |                                                        |
| `data-popup-open`, `data-pressed`          | a trigger       | Base UI writes them                                    |
| `data-slot="navigation-menu-trigger-icon"` | the chevron     | Rotates while its item is open                         |
| `data-slot="navigation-menu-content"`      | a panel         |                                                        |
| `data-activation-direction`                | a panel         | Which side the previous trigger was on; sets the slide |
| `data-slot="navigation-menu-link"`         | a link          |                                                        |
| `data-active`                              | a link          | Mirrors `active`                                       |
| `data-slot="navigation-menu-positioner"`   | the positioner  | Slides between triggers                                |
| `data-side`, `data-align`, `data-instant`  | the positioner  | Base UI writes them                                    |
| `data-slot="navigation-menu-popup"`        | the popup       | The surface: fill, ring, radius                        |
| `data-slot="navigation-menu-viewport"`     | the viewport    | Clips the panel while it slides                        |
| `data-starting-style`, `data-ending-style` | popup and panel | Base UI writes them while either animates              |
