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,
],
);
}
}
class TabsBadgeExample extends StatefulComponent {
const TabsBadgeExample({super.key});
@override
State<TabsBadgeExample> createState() => _TabsBadgeExampleState();
}
class _TabsBadgeExampleState extends State<TabsBadgeExample> {
static const _contents = [
'You have 3 unread messages.',
'Nothing sent yet.',
'Drafts are saved automatically.',
];
int _index = 0;
@override
Component build(BuildContext context) {
return div(
[
Tabs(
tabs: [
CoreTabItem(
label: 'Inbox',
badge: Badge(form: CoreBadgeForm.count, child: Text('3')),
),
const CoreTabItem(label: 'Sent'),
const CoreTabItem(label: 'Drafts'),
],
selectedIndex: _index,
onChanged: (index) => setState(() => _index = index),
),
Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
classes: 'flex flex-col items-start',
);
}
}
class TabsBadgeExample extends StatefulWidget {
const TabsBadgeExample({super.key});
@override
State<TabsBadgeExample> createState() => _TabsBadgeExampleState();
}
class _TabsBadgeExampleState extends State<TabsBadgeExample> {
static const _contents = [
'You have 3 unread messages.',
'Nothing sent yet.',
'Drafts are saved automatically.',
];
int _index = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Tabs(
tabs: const [
CoreTabItem(
label: 'Inbox',
badge: Badge(form: CoreBadgeForm.count, child: Text('3')),
),
CoreTabItem(label: 'Sent'),
CoreTabItem(label: 'Drafts'),
],
selectedIndex: _index,
onChanged: (i) => setState(() => _index = i),
),
const Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
);
}
}
class TabsChainExample extends StatefulComponent {
const TabsChainExample({super.key});
@override
State<TabsChainExample> createState() => _TabsChainExampleState();
}
class _TabsChainExampleState extends State<TabsChainExample> {
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),
)
.withStyle(
const CoreTabsStyle(
tabPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space8,
),
containerRadius: CoreBorderRadius.all(CoreRadius.radius24),
tabSpacing: CoreSpace.space8,
),
)
.surfaceContainer,
Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
classes: 'flex flex-col items-start',
);
}
}
class TabsChainExample extends StatefulWidget {
const TabsChainExample({super.key});
@override
State<TabsChainExample> createState() => _TabsChainExampleState();
}
class _TabsChainExampleState extends State<TabsChainExample> {
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.start,
children: [
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
onChanged: (i) => setState(() => _index = i),
)
.withStyle(
const CoreTabsStyle(
tabPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space8,
),
containerRadius: CoreBorderRadius.all(CoreRadius.radius24),
tabSpacing: CoreSpace.space8,
),
)
.surfaceContainer,
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) |
size |
CoreComponentSize |
md |
탭 항목 하나의 크기 —
CoreTabsStyle.defaultsBySize
의
itemHeight
항목을 고르는 시맨틱 식별자. 위젯 파라미터로만 정하며 테마로 오버라이드되지 않음 (높이 값 자체는
tabsStyle.itemHeight
로 오버라이드)
|
expand |
bool |
false |
탭이 전체 너비를 균등 분할 |
swipeable |
bool |
true |
좌우 스와이프로 탭 전환 |
tabsStyle |
CoreTabsStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
CoreTabItem#
tabs 목록의 항목 하나. Flutter / Web 동일한 데이터 타입입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
label | String | 필수 | 탭에 표시되는 텍스트 |
enabled | bool | true | 선택 가능 여부 |
badge |
W? (Widget? / Component?) |
null |
라벨 뒤에 그려지는 배지 — 보통
Badge(form: .count, child: Text('3'))
. 호출자가 완성된 위젯을 넘기는 합성 슬롯이고, 라벨과의 간격만
CoreTabsStyle.labelBadgeGapStyle
이 정합니다.
Tabs
/
TabPane
둘 다 렌더합니다
|
content |
W? (Widget? / Component?) |
null |
콘텐츠 패널 — TabPane 만 소비하고 Tabs 는 무시합니다 |
leading |
W? (Widget? / Component?) |
null |
라벨
앞
에 그려지는 위젯 — 에디터 탭의 파일 종류 아이콘 같은 것.
TabPane
만 소비하고
Tabs
는 무시합니다. 간격은
CoreTabPaneStyle.labelBadgeGapStyle
(탭 내부 공통 한 걸음)
|
trailing |
W? (Widget? / Component?) |
null |
라벨과
badge
뒤
에 그려지는 위젯 — 에디터 탭의 닫기 어피던스 같은 것.
TabPane
만 소비하고
Tabs
는 무시합니다. 간격은
leading
과 같은 슬롯
|
박스형 탭 + 콘텐츠 패널을 함께 그리는 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? |
Tab-bar row background colour. null → variant default. |
activeIndicatorColor |
CoreColor? |
Active-indicator (pill fill / underline) colour. null → variant default. |
activeIndicatorRadius |
CoreBorderRadius? |
Active-indicator border radius. null → token default. |
activeIndicatorThickness |
double? |
Active-indicator (underline) thickness (logical px). null → token default. |
borderColor |
CoreColor? |
Tab/content separator border colour (underline variant). null → variant default. |
borderWidth |
double? |
Tab/content separator border thickness (logical px; underline variant).
null
→ token default.
|
activeContentColor |
CoreColor? |
Active tab content colour. null → token default. |
inactiveContentColor |
CoreColor? |
Inactive tab content colour. null → variant default. |
tabBarPadding |
CoreEdgeInsets? |
Tab-bar container padding. null → variant default. |
tabPadding |
CoreEdgeInsets? |
Per-tab button padding. null → variant default. |
containerRadius |
CoreBorderRadius? |
Tab-bar container corner radius.
null
→ token default. Applies only to the
pill
variant.
|
indicatorTransitionDuration |
Duration? |
Active-indicator colour-transition duration.
null
→ token default ([defaultIndicatorTransitionDuration]).
|
swipeSensitivity |
double? |
Minimum horizontal drag distance / swipe velocity to trigger a tab switch (logical px / sec). A velocity value — not a layout dimension — so resolvers forward this unscaled.
null
→ token default ([defaultSwipeSensitivity]).
|
tabSpacing |
double? |
Inter-tab spacing (logical px) between adjacent tab buttons. Flutter applies this to
Row(spacing:)
; Web emits it as the flex
gap
inline CSS.
null
→ token default ([defaultTabSpacing]).
|
itemHeight |
double? |
Individual tab item height (logical px; Epic K axis 8-2).
null
→ the widget's
size
-keyed entry in [defaultsBySize]. Distinct from
CoreTabPaneStyle.defaultBarHeight
(the surrounding
TabPane
bar container) — see [defaultsBySize].
|
contentTextStyle |
CoreTextStyle? |
Content / tab-label text style override. |
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed per-tab
Clickable
(press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.
|
labelBadgeGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the 1-off spacer between a tab's label and its
CoreTabItem.badge
.
null
→ token default ([defaultLabelBadgeGapStyle]). Merged on top of that default and raw-forwarded to the
Gap
widget the tab composes.
|
CoreTabsStyle 변형별 기본값 (CoreTabVariantStyle)#
| 필드 | pill | underline |
|---|---|---|
activeIndicatorColor | surface | primary |
inactiveContentColor |
onSurface (opacity defaultInactiveContentOpacity) | onSurfaceVariant |
tabBarPadding |
CoreEdgeInsets.symmetric(horizontal: CoreSpace.space4, vertic… | CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16, verti… |
tabPadding |
CoreEdgeInsets.symmetric(horizontal: CoreSpace.space12, verti… | CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16, verti… |
backgroundColor | surfaceContainer | — |
borderColor | — | outline |
빠른 오버라이드 (Chain)#
이미 만든 Tabs 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.surfaceContainer처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(surfaceContainer ==
CoreColors.surfaceContainer) 어느 컴포넌트에서 써도 뜻이 갈리지 않으며, 위 예시처럼 withStyle 뒤에 이어붙일 수도 있습니다.
class TabsChainExample extends StatefulWidget {
const TabsChainExample({super.key});
@override
State<TabsChainExample> createState() => _TabsChainExampleState();
}
class _TabsChainExampleState extends State<TabsChainExample> {
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.start,
children: [
Tabs(
tabs: const [
CoreTabItem(label: 'Account'),
CoreTabItem(label: 'Password'),
],
selectedIndex: _index,
onChanged: (i) => setState(() => _index = i),
)
.withStyle(
const CoreTabsStyle(
tabPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space8,
),
containerRadius: CoreBorderRadius.all(CoreRadius.radius24),
tabSpacing: CoreSpace.space8,
),
)
.surfaceContainer,
const Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
);
}
}
class TabsChainExample extends StatefulComponent {
const TabsChainExample({super.key});
@override
State<TabsChainExample> createState() => _TabsChainExampleState();
}
class _TabsChainExampleState extends State<TabsChainExample> {
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),
)
.withStyle(
const CoreTabsStyle(
tabPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space8,
),
containerRadius: CoreBorderRadius.all(CoreRadius.radius24),
tabSpacing: CoreSpace.space8,
),
)
.surfaceContainer,
Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
classes: 'flex flex-col items-start',
);
}
}
변형 (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,
],
);
}
}
라인 탭#
콘텐츠와 하단 보더로 구분되고, 활성 탭 아래에 밑줄 인디케이터가 표시됩니다. 하단 보더는 탭 바 박스의 것이라 expand: false 여도 부모가 준 너비 전체에 그려지고, 탭 행만 그 안에서 시작 방향에 붙어 hug 합니다 (Flutter / Web 동일).
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.badge 에 Badge 를 넘기면 라벨 뒤에 그려집니다. 배지는 호출자가 완성해서 넘기는 합성 슬롯이라 자기 색을 직접 칠하고(라벨 색을 상속하지 않음), 라벨과의 간격만
CoreTabsStyle.labelBadgeGapStyle (기본 CoreSpace.space4) 이 정합니다. 배지가 없는 탭은 간격도 그리지 않습니다. 두 변형과
TabPane 모두 같은 모양으로 렌더합니다 (Flutter / Web 동일).
class TabsBadgeExample extends StatefulComponent {
const TabsBadgeExample({super.key});
@override
State<TabsBadgeExample> createState() => _TabsBadgeExampleState();
}
class _TabsBadgeExampleState extends State<TabsBadgeExample> {
static const _contents = [
'You have 3 unread messages.',
'Nothing sent yet.',
'Drafts are saved automatically.',
];
int _index = 0;
@override
Component build(BuildContext context) {
return div(
[
Tabs(
tabs: [
CoreTabItem(
label: 'Inbox',
badge: Badge(form: CoreBadgeForm.count, child: Text('3')),
),
const CoreTabItem(label: 'Sent'),
const CoreTabItem(label: 'Drafts'),
],
selectedIndex: _index,
onChanged: (index) => setState(() => _index = index),
),
Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
classes: 'flex flex-col items-start',
);
}
}
class TabsBadgeExample extends StatefulWidget {
const TabsBadgeExample({super.key});
@override
State<TabsBadgeExample> createState() => _TabsBadgeExampleState();
}
class _TabsBadgeExampleState extends State<TabsBadgeExample> {
static const _contents = [
'You have 3 unread messages.',
'Nothing sent yet.',
'Drafts are saved automatically.',
];
int _index = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Tabs(
tabs: const [
CoreTabItem(
label: 'Inbox',
badge: Badge(form: CoreBadgeForm.count, child: Text('3')),
),
CoreTabItem(label: 'Sent'),
CoreTabItem(label: 'Drafts'),
],
selectedIndex: _index,
onChanged: (i) => setState(() => _index = i),
),
const Gap.space16(),
Text(_contents[_index]).bodyMedium.onSurface,
],
);
}
}
Tabs(
tabs: [
CoreTabItem(
label: 'Inbox',
badge: Badge(form: CoreBadgeForm.count, child: Text('3')),
),
const CoreTabItem(label: 'Sent'),
],
selectedIndex: index,
onChanged: (i) => setState(() => index = i),
)
비활성화 탭#
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이 변경됨