ResponsiveTabs
먼저 보고, 그다음 계약을 읽습니다
뷰포트에 따라 데스크톱 pill 탭 ↔ 모바일 하단 고정 bottom-nav 로 전환.
Component intent의도와 경계 읽기
같은 tabs/value API 로 두 모드를 그린다. breakpoint 이상(데스크톱) 은 Radix Tabs primitive 위 pill 스타일, 미만(모바일) 은 화면 하단 fixed bottom-nav(아이콘 + 라벨 + 배지). SSR-safe — 첫 paint 는 데스크톱을 가정하고 matchMedia 구독 후 분기한다. 모바일 bottom-nav 는 safe-area-inset-bottom 을 반영해 iOS 노치/제스처 바를 침범하지 않는다. children 은 보통 <TabsContent value="..."/> 들이며, 데스크톱은 Radix 가 활성만 노출하고 모바일은 활성 value 와 매칭되는 자식만 렌더한다.
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { ResponsiveTabs, TabsContent } from "@axe/ui";<ResponsiveTabs ariaLabel="대시보드" defaultValue="overview" tabs={[ { value: "overview", label: "개요", icon: <HomeIcon /> }, { value: "alerts", label: "알림", icon: <BellIcon />, badge: 3 }, { value: "settings", label: "설정", icon: <GearIcon />, disabled: true }, ]}> <TabsContent value="overview">…</TabsContent> <TabsContent value="alerts">…</TabsContent> <TabsContent value="settings">…</TabsContent></ResponsiveTabs>import { ResponsiveTabs, TabsContent } from "@axe/ui";<ResponsiveTabs value={tab} onValueChange={setTab} breakpoint={640} tabs={[ { value: "list", label: "목록" }, { value: "map", label: "지도" }, ]}> <TabsContent value="list">…</TabsContent> <TabsContent value="map">…</TabsContent></ResponsiveTabs><div class="axe-responsive-tabs axe-responsive-tabs--mobile"> <div id="axe-rt-panel-overview" role="tabpanel" aria-labelledby="axe-rt-tab-overview" tabindex="0" class="axe-responsive-tabs__content">…</div> <nav class="axe-responsive-tabs__bottom-nav axe-responsive-tabs__bottom-nav--fixed axe-safe-area-bottom" role="tablist" aria-label="대시보드"> <button type="button" id="axe-rt-tab-overview" role="tab" aria-selected="true" aria-controls="axe-rt-panel-overview" tabindex="0" class="axe-responsive-tabs__bottom-btn axe-responsive-tabs__bottom-btn--active"> <span class="axe-responsive-tabs__bottom-label">개요</span> </button> <button type="button" id="axe-rt-tab-alerts" role="tab" aria-selected="false" aria-controls="axe-rt-panel-alerts" tabindex="-1" class="axe-responsive-tabs__bottom-btn"> <span class="axe-responsive-tabs__bottom-label">알림</span> <span class="axe-responsive-tabs__badge">3</span> </button> </nav></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
tabs | ResponsiveTabItem[] | 필수 | — | 탭 정의 배열. ResponsiveTabItem = { value, label, icon?, badge?: string | number, disabled? }. |
value | string | — | — | 활성 탭 value (controlled). |
defaultValue | string | — | — | uncontrolled 초기 value. 미지정 시 tabs[0].value. |
onValueChange | (value: string) => void | — | — | 탭 전환 시 호출. |
breakpoint | number | — | 768 | 데스크톱 → 모바일 분기점(px). 미만이면 bottom-nav. |
bottomNavAlwaysVisible | boolean | — | true | 모바일 bottom-nav 를 화면 하단 fixed 로. false 면 inline 배치. |
children | React.ReactNode | — | — | 보통 <TabsContent value="..."/> 자식들. 모바일은 활성 value 와 매칭되는 것만 렌더. |
className | string | — | — | 루트에 추가 className. |
ariaLabel | string | — | "주요 탐색" | 두 모드의 tablist(<nav>/Radix List)에 적용되는 aria-label. |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-responsive-tabs | stable | 루트(데스크톱=Radix Root, 모바일=<div>). |
.axe-responsive-tabs--mobile | stable | 모바일 모드 루트 변형. |
.axe-responsive-tabs__pills | stable | 데스크톱 pill 묶음(Radix List, role=tablist). |
.axe-responsive-tabs__pill | stable | 데스크톱 pill 트리거(Radix Trigger). 활성 = [data-state="active"]. |
.axe-responsive-tabs__pill-icon | stable | pill 안 아이콘 span(aria-hidden). |
.axe-responsive-tabs__content | stable | 패널 컨테이너(데스크톱/모바일 공용). |
.axe-responsive-tabs__bottom-nav | stable | 모바일 하단 네비 <nav>(role=tablist). |
.axe-responsive-tabs__bottom-nav--fixed | stable | 하단 고정 변형(bottomNavAlwaysVisible=true). |
.axe-responsive-tabs__bottom-btn | stable | 모바일 탭 <button>(role=tab). |
.axe-responsive-tabs__bottom-btn--active | stable | 활성 모바일 탭. |
.axe-responsive-tabs__bottom-icon | stable | 모바일 탭 아이콘 span(aria-hidden). |
.axe-responsive-tabs__bottom-label | stable | 모바일 탭 라벨 span. |
.axe-responsive-tabs__badge | stable | 탭 우상단 배지(알림 수 등). pill/bottom-btn 양쪽 공용. |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- 데스크톱: ← → 로 pill 이동 + Home/End (Radix roving)
- 모바일: ← → ↑ ↓ 로 탭 순환 이동, Home/End (자체 onKeyDown). disabled 탭은 건너뜀
데스크톱은 Radix Tabs(List/Trigger)가 role=tablist/tab·aria-selected·aria-controls·roving tabindex 를 부여. 모바일은 자체 ARIA tabs 패턴 — <nav role="tablist">, 각 <button role="tab" id="axe-rt-tab-{value}" aria-selected aria-controls="axe-rt-panel-{value}"> 에 roving tabindex(활성 0 / 나머지 -1), 패널은 <div role="tabpanel" id="axe-rt-panel-{value}" aria-labelledby="axe-rt-tab-{value}" tabIndex=0>. 아이콘 span 은 aria-hidden.
SSR 은 viewport 를 모르므로 첫 paint 는 데스크톱 가정 → hydration 후 matchMedia 로 모바일 전환(초기 mismatch 최소화). 하단 fixed 네비는 .axe-safe-area-bottom(공용 유틸)로 safe-area-inset-bottom 을 패딩에 반영해 iOS 홈 인디케이터를 침범하지 않는다.