본문으로 건너뛰기
내비게이션 / responsive-tabs

ResponsiveTabs

뷰포트에 따라 데스크톱 pill 탭 ↔ 모바일 하단 고정 bottom-nav 로 전환.
내비게이션responsive-tabsStatic specimenresponsive tabs반응형 탭bottom nav하단 네비
01 · Specimen

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

뷰포트에 따라 데스크톱 pill 탭 ↔ 모바일 하단 고정 bottom-nav 로 전환.

Workbench 불러오는 중…
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 와 매칭되는 자식만 렌더한다.

02 · Use

예제

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

01기본 (icon + badge)tsxfragment
기본 (icon + badge)
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>
02controlled + 낮은 breakpointtsxfragment
controlled + 낮은 breakpoint
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>
03모바일 bottom-nav (CSS-only 시각)htmlfragment
모바일 bottom-nav (CSS-only 시각)
<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>
03 · React

Props

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

이름타입필수기본값설명
tabsResponsiveTabItem[]필수탭 정의 배열. ResponsiveTabItem = { value, label, icon?, badge?: string | number, disabled? }.
valuestring활성 탭 value (controlled).
defaultValuestringuncontrolled 초기 value. 미지정 시 tabs[0].value.
onValueChange(value: string) => void탭 전환 시 호출.
breakpointnumber768데스크톱 → 모바일 분기점(px). 미만이면 bottom-nav.
bottomNavAlwaysVisiblebooleantrue모바일 bottom-nav 를 화면 하단 fixed 로. false 면 inline 배치.
childrenReact.ReactNode보통 <TabsContent value="..."/> 자식들. 모바일은 활성 value 와 매칭되는 것만 렌더.
classNamestring루트에 추가 className.
ariaLabelstring"주요 탐색"두 모드의 tablist(<nav>/Radix List)에 적용되는 aria-label.
04 · Any stack

.axe-* 클래스 계약

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

클래스안정성용도
.axe-responsive-tabsstable루트(데스크톱=Radix Root, 모바일=<div>).
.axe-responsive-tabs--mobilestable모바일 모드 루트 변형.
.axe-responsive-tabs__pillsstable데스크톱 pill 묶음(Radix List, role=tablist).
.axe-responsive-tabs__pillstable데스크톱 pill 트리거(Radix Trigger). 활성 = [data-state="active"].
.axe-responsive-tabs__pill-iconstablepill 안 아이콘 span(aria-hidden).
.axe-responsive-tabs__contentstable패널 컨테이너(데스크톱/모바일 공용).
.axe-responsive-tabs__bottom-navstable모바일 하단 네비 <nav>(role=tablist).
.axe-responsive-tabs__bottom-nav--fixedstable하단 고정 변형(bottomNavAlwaysVisible=true).
.axe-responsive-tabs__bottom-btnstable모바일 탭 <button>(role=tab).
.axe-responsive-tabs__bottom-btn--activestable활성 모바일 탭.
.axe-responsive-tabs__bottom-iconstable모바일 탭 아이콘 span(aria-hidden).
.axe-responsive-tabs__bottom-labelstable모바일 탭 라벨 span.
.axe-responsive-tabs__badgestable탭 우상단 배지(알림 수 등). pill/bottom-btn 양쪽 공용.
비-React 소비 노트
데스크톱 pill 은 Radix [data-state] 에 의존해 무JS 전환 불가(정적 markup 만). 모바일 bottom-nav 는 순수 <button> + 클래스라 CSS-only 로 재현 가능하다. --desktop 변형과 __pill-label 은 markup 에만 존재하는 훅으로 전용 CSS 규칙이 없다(스타일은 각각 루트/부모에서 상속).
05 · Inclusive

접근성

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

Keyboard
  • 데스크톱: ← → 로 pill 이동 + Home/End (Radix roving)
  • 모바일: ← → ↑ ↓ 로 탭 순환 이동, Home/End (자체 onKeyDown). disabled 탭은 건너뜀
ARIA

데스크톱은 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.

Notes

SSR 은 viewport 를 모르므로 첫 paint 는 데스크톱 가정 → hydration 후 matchMedia 로 모바일 전환(초기 mismatch 최소화). 하단 fixed 네비는 .axe-safe-area-bottom(공용 유틸)로 safe-area-inset-bottom 을 패딩에 반영해 iOS 홈 인디케이터를 침범하지 않는다.

06 · Judgment

권장 · 지양

권장
  • children 을 <TabsContent value="..."/> 로 주고 value 를 tabs 항목과 일치시킨다(모바일 매칭 기준).
  • 탭 개수는 하단 네비에 맞게 3~5개로 제한하고 라벨은 짧게.
  • PWA/모바일 표면에서는 bottomNavAlwaysVisible 를 유지해 safe-area 를 반영시킨다.
지양
  • aria-selected/role 을 직접 붙이지 말 것 — 두 모드 모두 자동 부여.
  • 패널 콘텐츠를 <TabsContent> 밖에 두지 말 것 — 모바일 value 매칭에서 누락된다.
검색창을 열면 컴포넌트 인덱스를 불러옵니다.