TopNav
먼저 보고, 그다음 계약을 읽습니다
상단 네비게이션 바 — 데스크톱 inline · 모바일 2-드로어.
Component intent의도와 경계 읽기
logo·items·right 세 슬롯을 받는 상단 바. 데스크톱(≥768px)은 세 슬롯을 가로 inline 으로 배치하고, 모바일(≤767.98px)은 같은 요소를 재배치해 [햄버거] nav items 좌 드로어 · [kebab] right 슬롯 우 드로어로 만든다(한 번에 하나만 열림). 본체는 server-compatible 하고 드로어 상호작용(focus trap·scroll lock·Esc)만 client 로 분리한다. items 를 두 번 렌더하지 않고 단일 요소를 CSS 로 재배치하는 것이 핵심 설계다.
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { TopNav, ThemeToggle } from "@axe/ui";<TopNav logo={<a className="axe-topnav__brand" href="/">AXE Labs</a>} items={[ { href: "/overview", label: "개요", active: true }, { href: "/tokens", label: "Tokens" }, ]} right={<ThemeToggle />} mobileLabels={{ right: "테마 도구", openRight: "테마 도구 열기", closeRight: "테마 도구 닫기", }}/><nav class="axe-topnav" aria-label="Main navigation"> <div class="axe-topnav__left"><a class="axe-topnav__brand" href="/">AXE Labs</a></div> <div class="axe-topnav__items"> <a class="axe-topnav__item" aria-current="page" href="/overview">개요</a> <a class="axe-topnav__item" href="/tokens">Tokens</a> </div> <div class="axe-topnav__right"><!-- ThemeToggle 등 --></div></nav>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
logo | React.ReactNode | — | — | 좌측 로고/워드마크 슬롯. .axe-topnav__left 안에 렌더. |
items | NavItem[] | — | [] | 중앙 네비 항목. NavItem = { href: string; label: string; active?: boolean }. 데스크톱 inline, 모바일 좌 드로어로 표시. |
right | React.ReactNode | — | — | 우측 슬롯 (ThemeToggle · NavMenu 등). 모바일에선 kebab(⋯) 우 드로어로. |
mobileLabels | Partial<TopNavMobileLabels> | — | — | 모바일 양 drawer의 dialog/open/close accessible label 6개를 locale에 맞게 교체. 미지정 값은 English default. |
isActive | (href: string) => boolean | — | — | 항목 active 판정 콜백. items[].active 가 우선하며, 없을 때만 이 함수로 resolve. server 단계에서 resolve 되어 client 로는 plain data 만 전달. |
mobileDrawer | boolean | — | — | @deprecated v0.6 부터 no-op. 모바일 2-드로어가 새 기본이라 더 분기하지 않는다. 호출부 호환용으로 시그니처만 유지, v0.7 제거 예정. |
...rest | React.HTMLAttributes<HTMLElement> | — | — | 나머지는 루트 <nav> 로 전파. |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-topnav | stable | nav 루트 바 (높이 --axe-topnav-height 에 고정). |
.axe-topnav__left | stable | 로고/브랜드 좌측 슬롯. |
.axe-topnav__items | stable | 중앙 항목 컨테이너 — 데스크톱 inline(가로 fade 스크롤) / 모바일 좌 드로어. |
.axe-topnav__item | stable | 개별 nav 링크. |
.axe-topnav__item--active | stable | 활성 항목 (aria-current="page" 와 동등 스타일). |
.axe-topnav__right | stable | 우측 슬롯 — 데스크톱 inline / 모바일 우 드로어. 한 번만 렌더. |
.axe-topnav__brand | stable | 로고 슬롯에 텍스트 워드마크를 쓸 때 붙이는 브랜드 식자 클래스 (Logo 컴포넌트 대체 가능). |
.axe-topnav__hamburger | internal | 모바일 좌 드로어 토글 버튼 (바 좌측, CSS 로 모바일만 표시). JS 구동. |
.axe-topnav__kebab | internal | 모바일 우 드로어 토글 버튼 (바 우측 36px plain 아이콘). JS 구동. |
.axe-topnav__drawer-close | internal | modal drawer 내부의 명시적 닫기 버튼. 키보드·터치·스위치/SR 사용자가 Esc 없이 닫을 수 있음. |
.axe-mobile-scrim | internal | 드로어 열림 시 반투명 scrim (공용 모바일 드로어 클래스). 탭 시 닫힘. |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- 데스크톱: 표준 링크 Tab 순회
- 모바일 드로어 열림 시 Esc 로 닫기
- 드로어 열림 중 Tab 이 패널 안에서 순환 (focus trap)
- 열릴 때 첫 focusable 로 focus, 닫힐 때 토글 버튼으로 focus 복원
- 각 drawer 내부의 명시적 닫기 버튼으로 Esc 없이 닫기
루트 <nav aria-label="Main navigation">. 활성 항목 = aria-current="page". 햄버거/kebab 버튼 = aria-expanded + aria-controls(드로어 id) + 내용 기반 동적 aria-label. 열린 패널 = role=dialog + aria-modal이며 배경 sibling은 inert.
드로어 열림 중 body scroll lock. 토글과 패널의 키보드 focus ring을 보존한다. scrim 탭 또는 항목 클릭 시 닫히며 desktop breakpoint로 커지면 modal/inert 상태를 해제한다.