Tabs | CoUI

Tabs

탭 컴포넌트

Tabs#

콘텐츠를 탭으로 구분하여 표시하는 컴포넌트입니다.

Live Preview#

Web
Manage your account settings.
Flutter
Loading Flutter...
TabPane(
  index: 0,
  labels: ['Account', 'Password'],
  children: [
    text('Account settings'),
    text('Password settings'),
  ],
)
TabPane(
  index: 0,
  labels: ['Account', 'Password'],
  children: [
    text('Account settings'),
    text('Password settings'),
  ],
)

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 관련된 콘텐츠를 여러 패널로 나누어 전환할 때
  • 같은 맥락의 다른 뷰를 제공할 때 (개요/상세/리뷰 등)
  • 페이지 이동 없이 콘텐츠를 전환할 때

대신 다른 컴포넌트를 사용하세요:

  • Accordion: 모든 섹션을 한 페이지에서 펼치고 접을 때
  • Navigation: 앱의 주요 페이지 간 이동일 때
  • Select: 옵션 선택이 목적이고 관련 콘텐츠 패널이 없을 때

기본 사용법 (Basic Usage)#

Tabs(
  onChanged: handleTabChange,
  tabs: [
    Tab(label: '개요'),
    Tab(label: '리뷰'),
    Tab(label: '관련 상품'),
  ],
  children: [
    OverviewPanel(),
    ReviewPanel(),
    RelatedProductsPanel(),
  ],
)

// 컨트롤러 사용
final tabController = TabController(length: 3);

Tabs(
  controller: tabController,
  tabs: [
    Tab(label: '탭 1'),
    Tab(label: '탭 2'),
    Tab(label: '탭 3'),
  ],
  children: [Tab1Content(), Tab2Content(), Tab3Content()],
)
Tabs(
  onChanged: handleTabChange,
  tabs: [
    Tab(label: '개요'),
    Tab(label: '리뷰'),
    Tab(label: '관련 상품'),
  ],
  children: [
    OverviewPanel(),
    ReviewPanel(),
    RelatedProductsPanel(),
  ],
)

Props / Parameters#

속성타입기본값설명
tabsList<Tab>필수탭 목록
childrenList<Widget>필수탭 콘텐츠
controller TabController? null 탭 컨트롤러
onChanged CoreValueChanged<int>? null 탭 변경 콜백
variant TabVariant line 탭 스타일 변형
initialIndexint0초기 선택 탭
tabsStyle CoreTabsStyle<Color>? / CoreTabsStyle<CoreColor>? null 인스턴스 스타일 (Style 시스템 참조)

스타일 시스템 (Style System)#

Tabs 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreTabsStyle<Clr> 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8). 시맨틱 enum (variant) 과 behaviour (tabs / index / onChanged / swipeable) 는 위젯/컴포넌트 파라미터로 직접 전달합니다.

시맨틱 vs 스타일#

  • 시맨틱 enum / behaviour: 위젯/컴포넌트 파라미터로 직접 (tabs, index, onChanged, variant, swipeable, padding)
  • chrome / dimensional / 슬롯 스타일: CoreTabsStyle 한 곳으로 (activeIndicatorColor / activeIndicatorThickness / activeIndicatorRadius / tabBarBackgroundColor / tabBarPaddingH / tabBarPaddingV / tabGap / tabButtonStyle / contentTextStyle)

Resolve chain#

design system default for tabs
  → CoreTabsTheme.style                        // 프로젝트 공통
  → parent component slot override
  → widget.tabsStyle                           // 인스턴스별

각 nested 슬롯 스타일 (tabButtonStyle / contentTextStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다.

슬롯 매핑 (CoreTabsStyle 9 필드)#

필드타입 (Flutter)적용 영역
activeIndicatorColorColor?활성 탭 인디케이터 (밑줄/필) 색
activeIndicatorThicknessdouble?인디케이터 두께 (px)
activeIndicatorRadiusdouble?인디케이터 모서리 반지름 (px)
tabBarBackgroundColorColor?탭 바 배경 색
tabBarPaddingHdouble?탭 좌/우 패딩 (px)
tabBarPaddingVdouble?탭 위/아래 패딩 (px)
tabGapdouble?탭 버튼 간격 (px)
tabButtonStyle CoreButtonStyle<Color>? 탭 버튼 chrome (현재 구현은 foregroundColor / labelStyle 적용; CoButton(variant: ghost) asChild 통합은 #1322 이월)
contentTextStyle CoreTextStyle<Color>? 탭 콘텐츠 텍스트 스타일

Migration#

기존 TabsTheme 의 평면 chrome (backgroundColor / borderRadius / containerPadding / tabPadding / activeColor / inactiveColor) 은 호환을 위해 유지되지만, 새 코드는 tabsStyle 슬롯을 사용하세요:

기존 (legacy theme 필드)새 위치
TabsTheme.backgroundColor tabsStyle.tabBarBackgroundColor 또는 CoreTabsTheme.style.tabBarBackgroundColor
TabsTheme.activeColortabsStyle.activeIndicatorColor
TabsTheme.containerPadding (vertical) tabsStyle.tabBarPaddingV
TabsTheme.containerPadding (horizontal) tabsStyle.tabBarPaddingH
인스턴스 탭 텍스트 스타일 (불가능)tabsStyle.tabButtonStyle.labelStyle
인스턴스 콘텐츠 텍스트 스타일 (불가능)tabsStyle.contentTextStyle

사용 예 (Flutter)#

Tabs(
  index: currentTabIndex,
  onChanged: (index) => setState(() => currentTabIndex = index),
  labels: ['Account', 'Password'],
  tabsStyle: CoreTabsStyle(
    activeIndicatorColor: Colors.blue,
    activeIndicatorRadius: 8,
    tabBarBackgroundColor: Color(0xFFF7F7F7),
    tabBarPaddingH: 12,
    tabBarPaddingV: 6,
    tabGap: 4,
  ),
)

사용 예 (Web)#

TabList(
  index: currentTabIndex,
  onChanged: handleChange,
  labels: ['Account', 'Password'],
  tabsStyle: CoreTabsStyle<CoreColor>(
    activeIndicatorThickness: 2,
    tabBarPaddingH: 16,
    tabBarPaddingV: 8,
  ),
)

변형 (Variants)#

라인 탭#

Web
Flutter
Loading Flutter...
TabPane(
  index: 0,
  labels: ['Account', 'Password'],
  children: [
    text('Account settings'),
    text('Password settings'),
  ],
)
TabPane(
  index: 0,
  labels: ['Account', 'Password'],
  children: [
    text('Account settings'),
    text('Password settings'),
  ],
)

박스 탭#

Web
Manage your account settings.
Flutter
Loading Flutter...
TabPane(
  index: 0,
  labels: ['Account', 'Password'],
  children: [
    text('Account settings'),
    text('Password settings'),
  ],
)
TabPane(
  index: 0,
  labels: ['Account', 'Password'],
  children: [
    text('Account settings'),
    text('Password settings'),
  ],
)

필 탭#

Web
Manage your account settings.
Flutter
Loading Flutter...
TabPane(
  index: 0,
  labels: ['Account', 'Password'],
  children: [
    text('Account settings'),
    text('Password settings'),
  ],
)
TabPane(
  index: 0,
  labels: ['Account', 'Password'],
  children: [
    text('Account settings'),
    text('Password settings'),
  ],
)

아이콘 포함#

Tab(
  label: '설정',
  icon: Icon(Icons.settings),
)

비활성화 탭#

Tab(label: '준비 중', enabled: false)

동작 스펙 (Behavior)#

탭 전환#

  • 탭 클릭 시 해당 콘텐츠 패널이 즉시 표시
  • 이전 패널은 숨겨지고 선택된 패널만 렌더링
  • onChanged 콜백으로 탭 전환 이벤트 감지

TabPane — 고급 탭#

드래그 앤 드롭으로 탭 순서를 변경할 수 있는 고급 탭 컨테이너입니다.

TabPane<MyData>(
  data: tabItems,
  selectedIndex: currentIndex,
  onFocusChanged: handleTabChange,
  onSorted: handleReorder,
  builder: (data) => TabChild(
    tab: Text(data.label),
    child: data.content,
  ),
)
  • 탭이 많아지면 자동으로 스크롤 가능 (fade edge 표시)
  • leading, trailing 위젯으로 탭 바에 추가 컨트롤 배치

테마 설정#

TabsTheme(
  backgroundColor: Colors.grey[100],
  activeColor: Colors.blue,
  inactiveColor: Colors.grey,
  expand: true,  // 탭이 전체 너비를 채움
)

사용 가이드라인 (Usage Guidelines)#

✅ Do#

탭 레이블은 짧고 명확하게 작성하세요.

Tabs(
  tabs: [
    Tab(label: '개요'),
    Tab(label: '리뷰'),
    Tab(label: '사양'),
  ],
  children: [overviewPanel, reviewPanel, specPanel],
)

한두 단어로 각 탭의 내용을 명확히 전달합니다.


❌ Don't#

탭 레이블이 너무 길거나 모호하지 않게 하세요.

Tabs(
  tabs: [
    Tab(label: '제품의 전반적인 개요 및 소개'),
    Tab(label: '사용자 리뷰 및 평가'),
  ],
  children: [overviewPanel, reviewPanel],
)

긴 레이블은 탭 바 공간을 차지하고 스캔하기 어렵습니다.

✅ Do#

관련 콘텐츠끼리 탭으로 묶으세요.

Tabs(
  tabs: [
    Tab(label: '기본 정보'),
    Tab(label: '보안 설정'),
    Tab(label: '알림 설정'),
  ],
  children: [profileForm, securityForm, notificationForm],
)

같은 맥락(설정)의 하위 카테고리를 탭으로 나누면 자연스럽습니다.


❌ Don't#

관계없는 콘텐츠를 탭으로 묶지 마세요.

Tabs(
  tabs: [
    Tab(label: '프로필'),
    Tab(label: '결제 내역'),
    Tab(label: '고객센터'),
  ],
  children: [profile, payments, support],
)

독립된 기능은 별도 페이지(Navigation)로 분리하세요.

✅ Do#

비활성화 탭은 이유를 알려주세요.

Tab(
  label: '분석 (프리미엄)',
  enabled: false,
  icon: Icon(Icons.lock),
)

잠금 아이콘으로 업그레이드가 필요함을 시각적으로 전달합니다.


❌ Don't#

설명 없이 탭을 비활성화하지 마세요.

Tab(label: '분석', enabled: false)

왜 접근할 수 없는지 알 수 없어 사용자가 혼란스럽습니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Tab탭 목록으로 포커스 이동
/ 이전/다음 탭으로 포커스 이동
Home / End첫/마지막 탭으로 포커스 이동
Enter / Space포커스된 탭 선택

스크린 리더#

  • Flutter: 탭 역할과 현재 선택 상태가 자동 전달. "탭 2/5, 선택됨" 형태로 읽힘
  • Web: role="tab", role="tabpanel", aria-selected, data-state 적용

ARIA 속성#

<div role="tablist">
  <button role="tab" aria-selected="true" data-state="active">탭 1</button>
  <button role="tab" aria-selected="false" data-state="inactive">탭 2</button>
</div>
<div role="tabpanel" data-state="active">콘텐츠 1</div>

크로스 플랫폼 차이점 (Platform Differences)#

v3.0부터 기본 API (enabled, onChanged 등)가 통일되었습니다. 아래는 플랫폼 고유 차이점만 나열합니다.

항목FlutterWeb
선택 모델인덱스 기반 (int)값 기반 (String) 또는 인덱스
탭 컨테이너 Tabs, TabList, TabPane Tabs + TabsList + TabsTrigger + TabsContent
탭 정의 TabChild(tab: widget, child: content) 별도 TabsTriggerTabsContent
드래그 정렬TabPane에서 지원없음
스크롤 가능TabPane 오버플로우 시 fade edge없음
테마TabsTheme, TabPaneThemeTailwind CSS 클래스
변형암묵적 (테마 스타일링)클래스 기반 (line, boxed)
확장TabsTheme.expandflex CSS 클래스
아이콘Widget으로 자유 배치텍스트 기반 (래핑 가능)
  • Accordion: 접고 펼 수 있는 콘텐츠 섹션. 모든 섹션을 동시에 볼 수 있어야 할 때 적합
  • Navigation: 앱의 주요 섹션 간 이동. 탭과 달리 URL이 변경됨

조합 예제#

// 제품 상세 페이지 패턴
Card(
  header: CardHeader(title: product.name),
  body: CardBody(
    child: Tabs(
      tabs: [
        Tab(label: '설명', icon: Icon(Icons.description)),
        Tab(label: '사양', icon: Icon(Icons.list)),
        Tab(label: '리뷰', icon: Icon(Icons.star)),
      ],
      children: [
        DescriptionPanel(product: product),
        SpecificationsPanel(product: product),
        ReviewsPanel(productId: product.id),
      ],
    ),
  ),
)