Tabs#
콘텐츠를 탭으로 구분하여 표시하는 컴포넌트입니다.
Live Preview#
class TabsDefaultExample extends StatefulComponent {
const TabsDefaultExample({super.key});
@override
State<TabsDefaultExample> createState() => _TabsDefaultExampleState();
}
class _TabsDefaultExampleState extends State<TabsDefaultExample> {
static const _contents = [
'Manage your account settings.',
'Change your password here.',
];
int _index = 0;
@override
Component build(BuildContext context) {
return div(
[
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
onChanged: (index) => setState(() => _index = index),
),
Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
classes: 'flex flex-col items-start',
);
}
}
class TabsDefaultExample extends StatefulWidget {
const TabsDefaultExample({super.key});
@override
State<TabsDefaultExample> createState() => _TabsDefaultExampleState();
}
class _TabsDefaultExampleState extends State<TabsDefaultExample> {
static const _contents = [
'Manage your account settings.',
'Change your password here.',
];
int _index = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
onChanged: (i) => setState(() => _index = i),
),
const Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
);
}
}
class TabsLineExample extends StatefulComponent {
const TabsLineExample({super.key});
@override
State<TabsLineExample> createState() => _TabsLineExampleState();
}
class _TabsLineExampleState extends State<TabsLineExample> {
static const _contents = [
'Manage your account settings.',
'Change your password here.',
];
int _index = 0;
@override
Component build(BuildContext context) {
return div(
[
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
variant: CoreTabVariant.underline,
onChanged: (index) => setState(() => _index = index),
),
Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
classes: 'flex flex-col items-stretch w-full',
);
}
}
class TabsLineExample extends StatefulWidget {
const TabsLineExample({super.key});
@override
State<TabsLineExample> createState() => _TabsLineExampleState();
}
class _TabsLineExampleState extends State<TabsLineExample> {
static const _contents = [
'Manage your account settings.',
'Change your password here.',
];
int _index = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
variant: CoreTabVariant.underline,
onChanged: (i) => setState(() => _index = i),
),
const Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 관련된 콘텐츠를 여러 패널로 나누어 전환할 때
- 같은 맥락의 다른 뷰를 제공할 때 (개요/상세/리뷰 등)
- 페이지 이동 없이 콘텐츠를 전환할 때
대신 다른 컴포넌트를 사용하세요:
Accordion: 모든 섹션을 한 페이지에서 펼치고 접을 때Navigation: 앱의 주요 페이지 간 이동일 때Select: 옵션 선택이 목적이고 관련 콘텐츠 패널이 없을 때
기본 사용법 (Basic Usage)#
Tabs 는 데이터 기반 탭 바입니다. tabs 에 CoreTabItem 목록을, selectedIndex
에 현재 선택 인덱스를 전달하고 onChanged 로 전환을 감지합니다. Flutter / Web 동일한 API 입니다.
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: index,
onChanged: (i) => setState(() => index = i),
)
문자열 배열로 간편하게 만들 수도 있습니다.
Tabs.labels(
labels: const ['Account', 'Password'],
selectedIndex: index,
onChanged: (i) => setState(() => index = i),
)
Props / Parameters#
Tabs#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
tabs | List<CoreTabItem> | 필수 | 탭 목록 |
selectedIndex | int | 필수 | 현재 선택된 탭 인덱스 |
onChanged |
CoreValueChanged<int>? |
null |
탭 변경 콜백 |
variant |
CoreTabVariant |
pill |
탭 스타일 변형 (pill / underline) |
expand |
bool |
false |
탭이 전체 너비를 균등 분할 |
swipeable |
bool |
true |
좌우 스와이프로 탭 전환 |
tabsStyle |
CoreTabsStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
박스형 탭 + 콘텐츠 패널을 함께 그리는 TabPane 은 별도 컴포넌트입니다 (자체
content슬롯 ·reorderable·CoreTabPaneStyle).
스타일 시스템 (Style System)#
Tabs 의 모든 chrome / dimensional 오버라이드는 CoreTabsStyle 단일 슬롯으로 흐릅니다. 시맨틱 enum (variant) 과 behaviour (tabs
/ selectedIndex / onChanged / expand / swipeable) 는 위젯/컴포넌트 파라미터로 직접 전달합니다.
Resolve chain#
CoreTabsStyle.defaultX / defaultsByVariant // 디자인 시스템 기본값
→ CoreTabsTheme.style // 프로젝트 공통
→ CoreTabsTheme.variantStyles[variant] // 프로젝트 variant 별
→ widget.tabsStyle // 인스턴스별
CoreTabsStyle 필드#
| 필드 | 타입 | 적용 영역 |
|---|---|---|
backgroundColor |
CoreColor? |
탭 바 배경 색 (pill) |
activeIndicatorColor | CoreColor? | 활성 인디케이터 (필/밑줄) 색 |
activeIndicatorRadius |
CoreBorderRadius? |
인디케이터 모서리 반지름 (pill) |
activeIndicatorThickness |
double? |
인디케이터 두께 (underline) |
borderColor |
CoreColor? |
탭/콘텐츠 구분 보더 색 (underline) |
borderWidth |
double? |
탭/콘텐츠 구분 보더 두께 (underline) |
activeContentColor | CoreColor? | 활성 탭 텍스트 색 |
inactiveContentColor | CoreColor? | 비활성 탭 텍스트 색 |
tabBarPadding | CoreEdgeInsets? | 탭 바 패딩 |
tabPadding | CoreEdgeInsets? | 탭 버튼 패딩 |
containerRadius |
CoreBorderRadius? |
탭 바 컨테이너 모서리 반지름 (pill) |
tabSpacing | double? | 탭 버튼 간격 (logical px) |
indicatorTransitionDuration |
Duration? |
활성 인디케이터 전환 시간 |
contentTextStyle | CoreTextStyle? | 탭 라벨 텍스트 스타일 |
clickableStyle |
CoreClickableStyle? |
탭 버튼 인터랙션 chrome (press scale · 포커스 링 · disabled opacity) — 합성된 Clickable 로 raw-forward |
변형 (Variants)#
필 탭 (default)#
둥근 컨테이너 안에 활성 탭을 채워진 "필" 인디케이터로 강조합니다.
class TabsDefaultExample extends StatefulComponent {
const TabsDefaultExample({super.key});
@override
State<TabsDefaultExample> createState() => _TabsDefaultExampleState();
}
class _TabsDefaultExampleState extends State<TabsDefaultExample> {
static const _contents = [
'Manage your account settings.',
'Change your password here.',
];
int _index = 0;
@override
Component build(BuildContext context) {
return div(
[
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
onChanged: (index) => setState(() => _index = index),
),
Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
classes: 'flex flex-col items-start',
);
}
}
class TabsDefaultExample extends StatefulWidget {
const TabsDefaultExample({super.key});
@override
State<TabsDefaultExample> createState() => _TabsDefaultExampleState();
}
class _TabsDefaultExampleState extends State<TabsDefaultExample> {
static const _contents = [
'Manage your account settings.',
'Change your password here.',
];
int _index = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
onChanged: (i) => setState(() => _index = i),
),
const Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
);
}
}
라인 탭#
콘텐츠와 하단 보더로 구분되고, 활성 탭 아래에 밑줄 인디케이터가 표시됩니다.
class TabsLineExample extends StatefulComponent {
const TabsLineExample({super.key});
@override
State<TabsLineExample> createState() => _TabsLineExampleState();
}
class _TabsLineExampleState extends State<TabsLineExample> {
static const _contents = [
'Manage your account settings.',
'Change your password here.',
];
int _index = 0;
@override
Component build(BuildContext context) {
return div(
[
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
variant: CoreTabVariant.underline,
onChanged: (index) => setState(() => _index = index),
),
Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
classes: 'flex flex-col items-stretch w-full',
);
}
}
class TabsLineExample extends StatefulWidget {
const TabsLineExample({super.key});
@override
State<TabsLineExample> createState() => _TabsLineExampleState();
}
class _TabsLineExampleState extends State<TabsLineExample> {
static const _contents = [
'Manage your account settings.',
'Change your password here.',
];
int _index = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
variant: CoreTabVariant.underline,
onChanged: (i) => setState(() => _index = i),
),
const Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
);
}
}
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: index,
variant: CoreTabVariant.underline,
onChanged: (i) => setState(() => index = i),
)
박스형 탭 + 통합 콘텐츠 패널이 필요하면 별도 컴포넌트 TabPane 을 사용하세요.
비활성화 탭#
CoreTabItem(label: '준비 중', enabled: false)
동작 스펙 (Behavior)#
탭 전환#
- 탭 클릭 시 해당 콘텐츠 패널이 즉시 표시
onChanged콜백으로 탭 전환 이벤트 감지- 활성 인디케이터는
CoreDuration.ms100으로 부드럽게 전환
스와이프#
swipeable: true(기본값) 일 때 탭 바에서 좌우 스와이프로 다음/이전 탭 이동- 임계값은
CoreTabTokens.swipeSensitivity(20 logical px)
테마 설정#
CoreComponentTheme(
tabs: CoreTabsTheme(
style: CoreTabsStyle(
tabSpacing: CoreSpace.space4,
),
),
)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
탭 레이블은 짧고 명확하게 작성하세요.
Tabs(
tabs: const [
CoreTabItem(label: '개요'),
CoreTabItem(label: '리뷰'),
CoreTabItem(label: '사양'),
],
selectedIndex: index,
onChanged: handleTabChange,
)
한두 단어로 각 탭의 내용을 명확히 전달합니다.
❌ Don't#
탭 레이블이 너무 길거나 모호하지 않게 하세요.
Tabs(
tabs: const [
CoreTabItem(label: '제품의 전반적인 개요 및 소개'),
CoreTabItem(label: '사용자 리뷰 및 평가'),
],
selectedIndex: index,
onChanged: handleTabChange,
)
긴 레이블은 탭 바 공간을 차지하고 스캔하기 어렵습니다.
✅ Do#
관련 콘텐츠끼리 탭으로 묶으세요.
Tabs(
tabs: const [
CoreTabItem(label: '기본 정보'),
CoreTabItem(label: '보안 설정'),
CoreTabItem(label: '알림 설정'),
],
selectedIndex: index,
onChanged: handleTabChange,
)
같은 맥락(설정)의 하위 카테고리를 탭으로 나누면 자연스럽습니다.
❌ Don't#
관계없는 콘텐츠를 탭으로 묶지 마세요.
독립된 기능은 별도 페이지(Navigation)로 분리하세요.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
Tab | 탭 목록으로 포커스 이동 |
Enter / Space | 포커스된 탭 선택 |
스크린 리더#
- Flutter:
Semantics로 탭 역할과 현재 선택 상태가 전달 -
Web:
role="tab",role="tablist",role="tabpanel",aria-selected적용
ARIA 속성#
<div role="tablist">
<button role="tab" aria-selected="true">탭 1</button>
<button role="tab" aria-selected="false">탭 2</button>
</div>
크로스 플랫폼 차이점 (Platform Differences)#
Tabs / TabPane 은 Flutter / Web 에서 동일한 named properties API
를 사용합니다. 파라미터 이름·구조·동작이 양쪽 동일합니다.
| 항목 | Flutter | Web |
|---|---|---|
| 드래그 정렬 | Draggable / DragTarget | HTML5 drag-and-drop |
| 가로 스크롤 | FadeScroll + SingleChildScrollView |
overflow-x-auto |
| 스와이프 | GestureDetector.onHorizontalDragEnd |
touchstart / touchend |
관련 컴포넌트 (Related Components)#
- Accordion: 접고 펼 수 있는 콘텐츠 섹션. 모든 섹션을 동시에 볼 수 있어야 할 때 적합
- Navigation: 앱의 주요 섹션 간 이동. 탭과 달리 URL이 변경됨