Tabs
APG Tabs 패턴 컴파운드 컴포넌트 — 비제어(defaultValue) 또는 제어(value) 선택 상태, roving tabindex, 화살표 자동 활성화.
마지막 업데이트 2026-06-11
한눈에#
한 영역에서 한 번에 한 뷰만 보이며 콘텐츠 패널을 전환하는 APG Tabs입니다 — 화살표로 이동하면 즉시 활성화됩니다(자동 활성화).
- APG Tabs 패턴
- roving tabindex
- 화살표 자동 활성화
- 제어·비제어
선택 상태는 Tabs 루트가 컨텍스트로 소유 — 패널 전환에 별도 배선이 필요 없습니다
사용 시점#
한 번에 한 뷰만 보이며 패널을 전환하면 Tabs — 동시 비교나 옵션 토글은 다른 컴포넌트입니다.
개요·사용법·변경 이력처럼 상호 배타적인 콘텐츠 패널을 한 번에 하나씩 전환
플레이그라운드#
컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.
import { Tabs } from '@wds/ui-web';
<Tabs
defaultValue="overview"
>
<Tabs.List aria-label="프로젝트 섹션">
<Tabs.Trigger value="overview">개요</Tabs.Trigger>
<Tabs.Trigger value="members">멤버</Tabs.Trigger>
<Tabs.Trigger value="settings">설정</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="overview">개요 내용</Tabs.Content>
<Tabs.Content value="members">멤버 내용</Tabs.Content>
<Tabs.Content value="settings">설정 내용</Tabs.Content>
</Tabs>해부#
트리거(탭 버튼)가 측정 단위입니다 — 패딩과 활성 인디케이터를 핀으로 표기합니다.
- padding-block
- 12px
space-3 - padding-inline
- 16px
space-4 - gap
- 8px
space-2 - font
- 16 / 500
body-1 · medium - active indicator
- 2px
color.primary - list divider
- 1px
color.border
변형#
단일 변형입니다 — 활성 인디케이터는 하단 보더(primary) 한 가지 어휘만
사용합니다. 비활성 탭은 네이티브 disabled로 표현하며 키보드 순회에서
제외됩니다.
크기#
단일 크기입니다 — 트리거 패딩은 space.3/4, 레이블은 font-size.body-1 고정.
3단 size는 component 토큰 신설 기준(§4) 미충족으로 도입하지 않았습니다.
상태#
비제어/제어 컴파운드입니다 — 기본은 비제어로 <Tabs defaultValue>가 선택 상태를
소유하고 하위 파트(List/Trigger/Content)는 컨텍스트로 소비합니다. value를 주면 제어
모드가 되어 부모가 상태를 소유하고, 탭 클릭·화살표 자동 활성화 등 모든 전환은 내부
상태를 쓰지 않고 onValueChange(value)로만 통지됩니다(렌더는 부모 value를 따름).
Hover/Focus/Disabled는 CSS 의사클래스, 선택 상태는 aria-selected 속성 셀렉터로 표현됩니다.
Props#
점 표기(Tabs.List)와 평탄 이름(TabsList)은 동일 함수입니다 — 서버
컴포넌트(RSC)에서는 클라이언트 참조에 점 접근이 해석되지 않으므로 평탄
이름을 사용하세요(이 문서의 데모도 평탄 이름).
<Tabs> (루트)
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | — | 제어 선택 탭 value — 제공하면 제어 모드(내부 상태 무시, 전환이 onValueChange로 통지) |
defaultValue | string | — | 초기 선택 탭 value — 이후 상태는 내부 소유(비제어, value 미제공 시) |
onValueChange | (value: string) => void | — | 선택 변경 콜백 — 제어/비제어 모두에서 호출 |
className | string | — | 루트 클래스 합성 |
<Tabs.List>
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
aria-label | string | — | 탭 목록의 접근 가능한 이름 — APG 필수 |
variant | 'fixed' | 'scrollable' | — | 표시 방식 — 기본 'fixed'(균등 배치), 'scrollable'은 탭이 많을 때 가로 스크롤 (ADR-012 B-P1) |
level | 'primary' | 'secondary' | — | 위계 — 기본 'primary', 'secondary'는 작은 폰트·여백 (ADR-012 B-P1) |
className | string | — | 클래스 합성 |
<Tabs.Trigger> — 네이티브 button 속성 확장(disabled 등 전달)
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | — | 같은 value의 Content와 짝 — 필수 |
<Tabs.Content>
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | — | 대응하는 Trigger의 value — 필수 |
className | string | — | 클래스 합성 |
접근성#
| 계약 | 구현 |
|---|---|
| 역할 | tablist / tab / tabpanel |
| 이름 | List의 aria-label 필수, 패널은 aria-labelledby로 탭과 연결 |
| 선택 | aria-selected + aria-controls (useId 기반 id 자동 연결) |
| roving tabindex | 선택 탭만 tabindex=0 — Tab 한 번에 목록 진입/탈출 |
| 키보드 | ←/→ 순환 이동, Home/End 처음/끝 — 이동 즉시 자동 활성화(APG) |
| disabled | 네이티브 disabled — 클릭·화살표 순회 모두 제외 |
| 패널 도달 | 패널 tabIndex=0 — 내부 포커서블 없어도 키보드 도달 |
| 모션 | 패널 전환은 opacity/transform만 + prefers-reduced-motion 존중 |
토큰#
component 토큰 없이 semantic을 직접 소비합니다(신설 기준 §4 미충족).
| 속성 | 토큰 |
|---|---|
| 목록 구분선 | color.border |
| 트리거 텍스트 | color.text-muted → hover color.text → disabled color.text-placeholder |
| 활성 탭 | color.primary-text(레이블) · color.primary(하단 인디케이터) |
| 포커스 링 | color.focus-ring · radius.sm |
| 타이포 | font.sans · font-size.body-1 · font-weight.medium · line-height.tight |
| 간격 | space.1/2/3/4 |
| 모션 | duration.fast · ease.standard / ease.out |