Navigation Menu

웹사이트를 탐색하기 위한 링크 모음 컴포넌트예요.

출처: 문서

본문

웹사이트 상단 내비게이션 바 같은 링크 모음 컴포넌트예요. 탭 포커스가 관리되는 유연한 레이아웃을 지원하고, 서브메뉴와 활성 아이템 인디케이터를 제공하며, 키보드 내비게이션을 완전히 지원해요. viewport와 CSS 변수를 통해 고급 애니메이션도 만들 수 있어요.

Features

  • 제어(controlled)/비제어(uncontrolled) 방식 모두 지원.
  • 탭 포커스가 관리되는 유연한 레이아웃 구조.
  • 서브메뉴 지원.
  • 선택적 활성 아이템 인디케이터.
  • 완전한 키보드 내비게이션.
  • 고급 애니메이션을 위한 CSS 변수 노출.
  • 커스텀 타이밍 지원.

Anatomy

모든 파트를 임포트해 조립해요.

import { NavigationMenu } from "radix-ui";

export default () => (
  <NavigationMenu.Root>
    <NavigationMenu.List>
      <NavigationMenu.Item>
        <NavigationMenu.Trigger />

        <NavigationMenu.Content>
          <NavigationMenu.Link />
        </NavigationMenu.Content>
      </NavigationMenu.Item>

      <NavigationMenu.Item>
        <NavigationMenu.Link />
      </NavigationMenu.Item>

      <NavigationMenu.Item>
        <NavigationMenu.Trigger />

        <NavigationMenu.Content>
          <NavigationMenu.Sub>
            <NavigationMenu.List />

            <NavigationMenu.Viewport />
          </NavigationMenu.Sub>
        </NavigationMenu.Content>
      </NavigationMenu.Item>

      <NavigationMenu.Indicator />
    </NavigationMenu.List>

    <NavigationMenu.Viewport />
  </NavigationMenu.Root>
);

API Reference

Root

내비게이션 메뉴의 모든 파트를 담아요.

Prop Type Default
defaultValue string No default value
value string No default value
onValueChange function No default value
delayDuration number 200
skipDelayDuration number 300
dir enum No default value
orientation enum "horizontal"
Data attribute Values
[data-orientation] "vertical" | "horizontal"

Sub

서브메뉴를 나타내요. 중첩해서 서브메뉴를 만들 때 root 파트 대신 사용해요.

Prop Type Default
defaultValue string No default value
value string No default value
onValueChange function No default value
orientation enum "horizontal"
Data attribute Values
[data-orientation] "vertical" | "horizontal"

List

최상위 메뉴 아이템을 담아요.

Prop Type Default
asChild boolean false
Data attribute Values
[data-orientation] "vertical" | "horizontal"

Item

최상위 메뉴 아이템으로, 링크 또는 트리거와 콘텐츠의 조합을 담아요.

Prop Type Default
asChild boolean false
value string No default value

Trigger

콘텐츠를 토글하는 버튼이에요.

Prop Type Default
asChild boolean false
Data attribute Values
[data-state] "open" | "closed"
[data-disabled] Present when disabled

Content

각 트리거와 연결된 콘텐츠를 담아요.

Prop Type Default
asChild boolean false
onEscapeKeyDown function No default value
onPointerDownOutside function No default value
onFocusOutside function No default value
onInteractOutside function No default value
forceMount boolean No default value
Data attribute Values
[data-state] "open" | "closed"
[data-motion] "to-start" | "to-end" | "from-start" | "from-end"
[data-orientation] "vertical" | "horizontal"

탐색 링크예요.

Prop Type Default
asChild boolean false
active boolean false
onSelect function No default value
Data attribute Values
[data-active] Present when active

Indicator

리스트 아래에 렌더링되는 선택적 인디케이터 요소로, 현재 활성 트리거를 강조하는 데 사용해요.

Prop Type Default
asChild boolean false
forceMount boolean No default value
Data attribute Values
[data-state] "visible" | "hidden"
[data-orientation] "vertical" | "horizontal"
CSS Variable Description
--radix-navigation-menu-indicator-translate-x The horizontal offset of the indicator, computed from the active trigger's position. Present when the menu is horizontal.
--radix-navigation-menu-indicator-translate-y The vertical offset of the indicator, computed from the active trigger's position. Present when the menu is vertical.

Viewport

리스트 바깥에 활성 콘텐츠를 렌더링하는 데 사용하는 선택적 뷰포트 요소예요.

Prop Type Default
asChild boolean false
forceMount boolean No default value
Data attribute Values
[data-state] "open" | "closed"
[data-orientation] "vertical" | "horizontal"
CSS Variable Description
--radix-navigation-menu-viewport-width The width of the viewport when visible/hidden, computed from the active content
--radix-navigation-menu-viewport-height The height of the viewport when visible/hidden, computed from the active content

Examples

세로 방향

orientation prop을 사용해 세로 메뉴를 만들 수 있어요.

<NavigationMenu.Root orientation="vertical">
  <NavigationMenu.List>
    <NavigationMenu.Item>
      <NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>

      <NavigationMenu.Content>Item one content</NavigationMenu.Content>
    </NavigationMenu.Item>

    <NavigationMenu.Item>
      <NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>

      <NavigationMenu.Content>Item Two content</NavigationMenu.Content>
    </NavigationMenu.Item>
  </NavigationMenu.List>
</NavigationMenu.Root>

유연한 레이아웃

Content가 어디에 렌더링될지에 대한 추가 제어가 필요하면 Viewport 파트를 사용해요. DOM 구조를 조정해야 하거나 고급 애니메이션을 위한 유연성이 필요할 때 유용해요. 탭 포커스는 자동으로 유지돼요.

<NavigationMenu.Root>
  <NavigationMenu.List>
    <NavigationMenu.Item>
      <NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>

      <NavigationMenu.Content>Item one content</NavigationMenu.Content>
    </NavigationMenu.Item>

    <NavigationMenu.Item>
      <NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>

      <NavigationMenu.Content>Item two content</NavigationMenu.Content>
    </NavigationMenu.Item>
  </NavigationMenu.List>

  {/* NavigationMenu.Content will be rendered here when active */}

  <NavigationMenu.Viewport />
</NavigationMenu.Root>

인디케이터와 함께

선택적 Indicator 파트로 현재 활성 Trigger를 강조할 수 있어요. Viewport와 함께 화살표나 하이라이트 같은 애니메이션 시각적 표시를 제공하고 싶을 때 유용해요.

// index.jsx

import { NavigationMenu } from "radix-ui";

import "./styles.css";

export default () => (
  <NavigationMenu.Root>
    <NavigationMenu.List>
      <NavigationMenu.Item>
        <NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>

        <NavigationMenu.Content>Item one content</NavigationMenu.Content>
      </NavigationMenu.Item>

      <NavigationMenu.Item>
        <NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>

        <NavigationMenu.Content>Item two content</NavigationMenu.Content>
      </NavigationMenu.Item>

      <NavigationMenu.Indicator className="NavigationMenuIndicator" />
    </NavigationMenu.List>

    <NavigationMenu.Viewport />
  </NavigationMenu.Root>
);
/* styles.css */

.NavigationMenuIndicator {
  background-color: grey;
}

.NavigationMenuIndicator[data-orientation="horizontal"] {
  height: 3px;
  transition: width, transform, 250ms ease;
}

서브메뉴와 함께

NavigationMenu을 중첩하고 Root 대신 Sub 파트를 사용해 서브메뉴를 만들어요. 서브메뉴는 Root 내비게이션 메뉴와 다르게 Tabs와 비슷해서 항상 하나의 아이템이 활성이어야 하므로 defaultValue를 지정해 주세요.

<NavigationMenu.Root>
  <NavigationMenu.List>
    <NavigationMenu.Item>
      <NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>

      <NavigationMenu.Content>Item one content</NavigationMenu.Content>
    </NavigationMenu.Item>

    <NavigationMenu.Item>
      <NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>

      <NavigationMenu.Content>
        <NavigationMenu.Sub defaultValue="sub1">
          <NavigationMenu.List>
            <NavigationMenu.Item value="sub1">
              <NavigationMenu.Trigger>Sub item one</NavigationMenu.Trigger>

              <NavigationMenu.Content>Sub item one content</NavigationMenu.Content>
            </NavigationMenu.Item>

            <NavigationMenu.Item value="sub2">
              <NavigationMenu.Trigger>Sub item two</NavigationMenu.Trigger>

              <NavigationMenu.Content>Sub item two content</NavigationMenu.Content>
            </NavigationMenu.Item>
          </NavigationMenu.List>
        </NavigationMenu.Sub>
      </NavigationMenu.Content>
    </NavigationMenu.Item>
  </NavigationMenu.List>
</NavigationMenu.Root>

클라이언트 사이드 라우팅과 함께

라우팅 패키지가 제공하는 Link 컴포넌트를 써야 한다면, 커스텀 컴포넌트로 NavigationMenu.Link와 조합하는 것을 권장해요. 접근성과 일관된 키보드 제어가 유지돼요. Next.js 예시:

// index.jsx

import { usePathname } from "next/navigation";

import NextLink from "next/link";

import { NavigationMenu } from "radix-ui";

import "./styles.css";

const Link = ({ href, ...props }) => {
  const pathname = usePathname();
  const isActive = href === pathname;

  return (
    <NavigationMenu.Link asChild active={isActive}>
      <NextLink href={href} className="NavigationMenuLink" {...props} />
    </NavigationMenu.Link>
  );
};

export default () => (
  <NavigationMenu.Root>
    <NavigationMenu.List>
      <NavigationMenu.Item>
        <Link href="/">Home</Link>
      </NavigationMenu.Item>

      <NavigationMenu.Item>
        <Link href="/about">About</Link>
      </NavigationMenu.Item>
    </NavigationMenu.List>
  </NavigationMenu.Root>
);
/* styles.css */

.NavigationMenuLink {
  text-decoration: none;
}

.NavigationMenuLink[data-active] {
  text-decoration: "underline";
}

고급 애니메이션

--radix-navigation-menu-viewport-[width|height]와 data-motion['from-start'|'to-start'|'from-end'|'to-end'] 속성을 노출해, 진입/퇴장 방향에 따라 Viewport 크기와 Content 위치를 애니메이션할 수 있어요.

이들을 position: absolute;와 결합하면 항목 간 이동 시 부드러운 오버랩 애니메이션 효과를 만들 수 있어요.

// index.jsx

import { NavigationMenu } from "radix-ui";

import "./styles.css";

export default () => (
  <NavigationMenu.Root>
    <NavigationMenu.List>
      <NavigationMenu.Item>
        <NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>

        <NavigationMenu.Content className="NavigationMenuContent">
          Item one content
        </NavigationMenu.Content>
      </NavigationMenu.Item>

      <NavigationMenu.Item>
        <NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>

        <NavigationMenu.Content className="NavigationMenuContent">
          Item two content
        </NavigationMenu.Content>
      </NavigationMenu.Item>
    </NavigationMenu.List>

    <NavigationMenu.Viewport className="NavigationMenuViewport" />
  </NavigationMenu.Root>
);
/* styles.css */

.NavigationMenuContent {
  position: absolute;
  top: 0;
  left: 0;
  animation-duration: 250ms;
  animation-timing-function: ease;
}

.NavigationMenuContent[data-motion="from-start"] {
  animation-name: enterFromLeft;
}

.NavigationMenuContent[data-motion="from-end"] {
  animation-name: enterFromRight;
}

.NavigationMenuContent[data-motion="to-start"] {
  animation-name: exitToLeft;
}

.NavigationMenuContent[data-motion="to-end"] {
  animation-name: exitToRight;
}

.NavigationMenuViewport {
  position: relative;
  width: var(--radix-navigation-menu-viewport-width);
  height: var(--radix-navigation-menu-viewport-height);
  transition: width, height, 250ms ease;
}

@keyframes enterFromRight {
  from {
    opacity: 0;
    transform: translateX(200px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes enterFromLeft {
  from {
    opacity: 0;
    transform: translateX(-200px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes exitToRight {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(200px);
  }
}

@keyframes exitToLeft {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(-200px);
  }
}

Accessibility

navigation role 요구사항을 준수해요.

NavigationMenu는 menubar와 혼동해서는 안 돼요. 이 primitive는 일상적인 의미로 menu라는 이름을 공유해 탐색 링크 집합을 가리키지만, WAI-ARIA menu role을 사용하지 않아요. menu와 menubar는 데스크톱 애플리케이션 창에서 흔히 볼 수 있는 네이티브 OS 메뉴처럼 동작해서, 복합 포커스 관리·첫 문자 내비게이션 같은 복잡한 기능을 갖기 때문이에요.

이런 기능들은 웹사이트 탐색에는 불필요하다고 여겨지며, 최악의 경우 확립된 웹사이트 패턴에 익숙한 사용자를 혼란스럽게 할 수 있어요.

자세한 내용은 W3C Disclosure Navigation Menu 예시를 참고하세요.

링크 사용과 aria-current

메뉴 안의 모든 탐색 링크에 NavigationMenu.Link를 사용하는 것이 중요해요. 주 리스트뿐 아니라 NavigationMenu.Content로 렌더링되는 콘텐츠 안에서도 마찬가지예요. 이렇게 해야 일관된 키보드 상호작용과 접근성이 유지되고, active prop으로 aria-current와 활성 스타일도 설정할 수 있어요. 서드파티 라우팅 컴포넌트와의 사용법은 이 예시를 참고하세요.

키보드 상호작용

Key Description
Space``Enter When focus is on NavigationMenu.Trigger, opens the content.
Tab Moves focus to the next focusable element.
ArrowDown When horizontal and focus is on an open NavigationMenu.Trigger, moves focus into NavigationMenu.Content. Moves focus to the next NavigationMenu.Trigger or NavigationMenu.Link.
ArrowUp Moves focus to the previous NavigationMenu.Trigger or NavigationMenu.Link.
ArrowRight``ArrowLeft When vertical and focus is on an open NavigationMenu.Trigger, moves focus into its NavigationMenu.Content. Moves focus to the next / previous NavigationMenu.Trigger or NavigationMenu.Link.
Home``End Moves focus to the first/last NavigationMenu.Trigger or NavigationMenu.Link.
Esc Closes open NavigationMenu.Content and moves focus to its NavigationMenu.Trigger.

더 알아보기 (Learn more)

  • NavigationMenu는 nav 요소(WAI-ARIA navigation role) 기반이라 menubar의 menu role과 다르게 동작해요.
  • 서브메뉴(Sub)는 Tabs처럼 항상 하나가 활성이어야 하므로 defaultValue를 지정하는 것이 좋아요.