Tabs | CoUI

Tabs

탭 컴포넌트

Tabs#

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

Live Preview#

사용 시기 (When to Use)#

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

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

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

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

기본 사용법 (Basic Usage)#

Tabs 는 데이터 기반 탭 바입니다. tabsCoreTabItem 목록을, 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#

속성타입기본값설명
tabsList<CoreTabItem>필수탭 목록
selectedIndexint필수현재 선택된 탭 인덱스
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)
activeIndicatorColorCoreColor?활성 인디케이터 (필/밑줄) 색
activeIndicatorRadius CoreBorderRadius? 인디케이터 모서리 반지름 (pill)
activeIndicatorThickness double? 인디케이터 두께 (underline)
borderColor CoreColor? 탭/콘텐츠 구분 보더 색 (underline)
borderWidth double? 탭/콘텐츠 구분 보더 두께 (underline)
activeContentColorCoreColor?활성 탭 텍스트 색
inactiveContentColorCoreColor?비활성 탭 텍스트 색
tabBarPaddingCoreEdgeInsets?탭 바 패딩
tabPaddingCoreEdgeInsets?탭 버튼 패딩
containerRadius CoreBorderRadius? 탭 바 컨테이너 모서리 반지름 (pill)
tabSpacingdouble?탭 버튼 간격 (logical px)
indicatorTransitionDuration Duration? 활성 인디케이터 전환 시간
contentTextStyleCoreTextStyle?탭 라벨 텍스트 스타일
clickableStyle CoreClickableStyle? 탭 버튼 인터랙션 chrome (press scale · 포커스 링 · disabled opacity) — 합성된 Clickable 로 raw-forward

변형 (Variants)#

필 탭 (default)#

둥근 컨테이너 안에 활성 탭을 채워진 "필" 인디케이터로 강조합니다.

Web
Manage your account settings.
Flutter
Loading Flutter...
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,
      ],
    );
  }
}

라인 탭#

콘텐츠와 하단 보더로 구분되고, 활성 탭 아래에 밑줄 인디케이터가 표시됩니다.

Web
Manage your account settings.
Flutter
Loading Flutter...
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 를 사용합니다. 파라미터 이름·구조·동작이 양쪽 동일합니다.

항목FlutterWeb
드래그 정렬Draggable / DragTargetHTML5 drag-and-drop
가로 스크롤 FadeScroll + SingleChildScrollView overflow-x-auto
스와이프 GestureDetector.onHorizontalDragEnd touchstart / touchend
  • Accordion: 접고 펼 수 있는 콘텐츠 섹션. 모든 섹션을 동시에 볼 수 있어야 할 때 적합
  • Navigation: 앱의 주요 섹션 간 이동. 탭과 달리 URL이 변경됨