본문으로 건너뛰기
내비게이션 / nav-menu

NavMenu

TopNav 우측에 얹는 통일된 드롭다운 메뉴 1종 (entity-selector · user-menu).
내비게이션nav-menuStatic specimennavmenunav menudropdownentity selector
01 · Specimen

먼저 보고, 그다음 계약을 읽습니다

TopNav 우측에 얹는 통일된 드롭다운 메뉴 1종 (entity-selector · user-menu).

Workbench 불러오는 중…
Component intent의도와 경계 읽기

"버튼 → 팝오버 → 선택 항목 + (선택) 헤더/푸터 액션" 이라는 동일 구조를 데이터(props)로만 분기하는 단일 컴포넌트다. items 에 active 를 주면 role=menuitemradio 로 렌더되어 entity-selector(작업 대상 전환)가 되고, footer 에 tone="danger" 액션을 두면 user-menu(로그아웃)가 된다. Radix 대신 자체 구현이라 a11y(aria-haspopup/expanded/checked·키보드·focus 복귀)와 click-outside 를 직접 처리한다. 서브메뉴/타입어헤드 같은 무거운 사용처는 DropdownMenu(Radix)를 쓸 것.

02 · Use

예제

SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.

01entity-selector (active 표시)tsxfragment
entity-selector (active 표시)
import { NavMenu } from "@axe/ui";<NavMenu  label="AXE Ventures"  ariaLabel="작업 대상 조직 선택"  accent  items={[    { key: "axev", label: "AXE Ventures", active: true },    { key: "axec", label: "AXE Capital", active: false, onSelect: () => switchOrg("axec") },  ]}/>
02user-menu (header + footer danger)tsxfragment
user-menu (header + footer danger)
import { NavMenu } from "@axe/ui";<NavMenu  label="강수훈"  ariaLabel="계정 메뉴"  header={{ label: "강수훈 · admin" }}  items={[    { key: "settings", label: "설정", href: "/settings" },  ]}  footer={[    { key: "logout", label: "로그아웃", tone: "danger", onSelect: signOut },  ]}/>
03CSS-only 열린 팝오버 (비-React)htmlfragment
CSS-only 열린 팝오버 (비-React)
<div class="axe-topnav__menu">  <button class="axe-topnav__menu-trigger" aria-haspopup="menu" aria-expanded="true">    <span class="axe-topnav__menu-trigger-label">AXE Ventures</span>  </button>  <div class="axe-topnav__menu-popover" role="menu" aria-label="조직 선택" data-align="right">    <button class="axe-topnav__menu-item axe-topnav__menu-item--active" role="menuitemradio" aria-checked="true">      <span class="axe-topnav__menu-item-label">AXE Ventures</span>    </button>    <button class="axe-topnav__menu-item" role="menuitemradio" aria-checked="false">      <span class="axe-topnav__menu-item-label">AXE Capital</span>    </button>  </div></div>
03 · React

Props

TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.

이름타입필수기본값설명
labelReact.ReactNode필수트리거 버튼 라벨 (현재 선택 entity / 사용자 이름 등).
itemsNavMenuItem[]필수선택 항목 배열. NavMenuItem = { key: string; label: React.ReactNode; meta?: React.ReactNode; active?: boolean; href?: string; onSelect?: () => void }. active 가 정의되면 role=menuitemradio + aria-checked 로 렌더, undefined 면 일반 menuitem. href 면 <a>, 아니면 <button>.
header{ label: React.ReactNode; items?: NavMenuFooterItem[] }items 위 헤더 영역 — 라벨(현재 계정 이름 등) + (선택) 액션, 아래 구분선. 3-그룹 레이아웃(헤더·items·footer)용.
footerNavMenuFooterItem[]구분선 아래 액션 영역. NavMenuFooterItem = { key: string; label: React.ReactNode; href?: string; onSelect?: () => void; tone?: "default" | "danger" }. tone="danger" = 파괴적 액션(로그아웃·계정 삭제).
align"left" | "right""right"팝오버 정렬 — trigger 기준.
ariaLabelstring필수스크린리더용 메뉴 라벨 (role=menu 의 aria-label). 필수.
accentbooleanfalseaccent 톤 트리거 (강조가 필요한 entity-selector 등).
emptyLabelReact.ReactNode"항목 없음"items 가 비었을 때 표시할 문구.
data-testidstring루트 wrapper 에 부착할 testid.
menuTestidstring팝오버 노드에 부착할 testid.
04 · Any stack

.axe-* 클래스 계약

React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.

클래스안정성용도
.axe-topnav__menustablewrapper (trigger + popover 위치 기준 root).
.axe-topnav__menu-triggerstable트리거 버튼 (data-state open/closed, data-accent).
.axe-topnav__menu-trigger-labelinternal트리거 라벨 span.
.axe-topnav__menu-chevroninternalchevron svg (open 시 CSS 로 회전).
.axe-topnav__menu-popoverstable드롭다운 팝오버 (role=menu, data-align left/right).
.axe-topnav__menu-itemstable항목/액션 (a 또는 button).
.axe-topnav__menu-item--activestable선택된 항목.
.axe-topnav__menu-item--dangerstable파괴적 액션 (tone="danger").
.axe-topnav__menu-item-labelinternal항목 라벨 span.
.axe-topnav__menu-item-metainternal우측 muted 태그 span (역할·count).
.axe-topnav__menu-emptyinternalitems 빈 상태 문구.
.axe-topnav__menu-headerstable헤더 그룹 컨테이너 (라벨 + 액션, 아래 구분선).
.axe-topnav__menu-header-labelinternal헤더 라벨.
.axe-topnav__menu-footerstable푸터 그룹 컨테이너 (구분선 위 액션 묶음).
비-React 소비 노트
팝오버 열림/닫힘·키보드 순환·click-outside·focus 복원은 모두 컴포넌트 JS(useState)에 의존한다. CSS-only 로는 위 예제처럼 정적 팝오버 모양만 재현 가능(토글 동작 없음). axe-topnav__menu* 네임스페이스는 TopNav right 슬롯에 얹히도록 설계된 공유 계약이다.
05 · Inclusive

접근성

키보드, ARIA, 구현 노트를 함께 검토합니다.

Keyboard
  • ↑/↓ 로 메뉴 항목 순환 (양끝에서 wrap)
  • Enter/Space 로 항목 실행 (네이티브 button/a)
  • Esc 로 닫고 트리거로 focus 복원
  • 열리면 첫 항목으로 자동 focus
ARIA

트리거 = aria-haspopup="menu" + aria-expanded + aria-controls(열림 시 popover id). 팝오버 = role="menu" + aria-label(=ariaLabel prop). 항목: active 정의 시 role="menuitemradio" + aria-checked, 아니면 role="menuitem"(href 항목은 active 시 aria-current="true"). header/footer 액션 = role="menuitem", 빈 상태 = role="presentation".

Notes

click-outside(pointerdown capture)로 닫힘. 항목 실행 후 자동 닫힘 + 트리거 focus 복원. reduced-motion 시 팝오버 등장 애니 없음. Radix 미사용 자체 구현, SSR-safe(초기 렌더는 닫힘).

06 · Judgment

권장 · 지양

권장
  • ariaLabel 은 항상 지정 (SR 필수).
  • entity-selector 는 items[].active 로 현재 선택을 표시.
  • 파괴적 액션(로그아웃)은 footer + tone="danger" 로.
지양
  • 서브메뉴/타입어헤드 같은 무거운 메뉴에 쓰지 말 것 → DropdownMenu(Radix) 사용.
  • label 없이 아이콘만 트리거로 쓰지 말 것 (트리거 라벨이 곧 접근명).
검색창을 열면 컴포넌트 인덱스를 불러옵니다.