Switcher | CoUI

Switcher

슬라이드 전환과 드래그 제스처로 여러 뷰 사이를 오가는 스위처 컴포넌트

Switcher#

여러 자식 뷰 중 하나를 한 번에 보여주고, 활성 인덱스가 바뀌면 슬라이드 / 페이드 전환을 애니메이션하는 컴포넌트입니다. 사용자는 드래그하여 인접한 뷰 사이를 스크럽할 수 있습니다.

Switcher는 레거시 Flutter Switcher와 Web Switcher를 통일한 컴포넌트입니다. Flutter와 Web이 동일한 파라미터 이름·동작·시각을 갖습니다 — 네 방향 슬라이드, 드래그 제스처, 동일한 전환 시간.

Live Preview#

Web
View A
Flutter
Loading Flutter...
class SwitcherDefaultExample extends StatefulComponent {
  const SwitcherDefaultExample({super.key});

  @override
  State<SwitcherDefaultExample> createState() =>
      _SwitcherDefaultExampleState();
}

class _SwitcherDefaultExampleState extends State<SwitcherDefaultExample> {
  int _index = 0;

  static const List<String> _labels = ['View A', 'View B', 'View C'];

  @override
  Component build(BuildContext context) {
    return div(
      [
        div(
          [
            Switcher(
              index: _index,
              onIndexChanged: (next) => setState(() => _index = next),
              items: [
                for (final label in _labels)
                  div(
                    [Text(label).titleMedium],
                    classes: 'flex items-center justify-center h-full',
                  ),
              ],
            ),
          ],
          styles: const Styles(
            raw: {'width': '16rem', 'height': '6rem'},
          ),
        ),
        Gap.space16(),
        div(
          [
            for (var idx = 0; idx < _labels.length; idx++)
              Button(
                variant: idx == _index
                    ? CoreButtonVariant.primary
                    : CoreButtonVariant.outline,
                onPressed: () => setState(() => _index = idx),
                child: Text(_labels[idx]),
              ),
          ],
          classes: 'flex flex-row flex-wrap justify-center gap-${CoreSpace.scale.space8}',
        ),
      ],
      classes: 'flex flex-col items-center',
    );
  }
}
class SwitcherDefaultExample extends StatefulWidget {
  const SwitcherDefaultExample({super.key});

  @override
  State<SwitcherDefaultExample> createState() =>
      _SwitcherDefaultExampleState();
}

class _SwitcherDefaultExampleState extends State<SwitcherDefaultExample> {
  int _index = 0;

  static const List<String> _labels = ['View A', 'View B', 'View C'];

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        SizedBox(
          width: CoreSize.size256,
          height: CoreSize.size96,
          child: Switcher(
            index: _index,
            onIndexChanged: (next) => setState(() => _index = next),
            items: [
              for (final label in _labels)
                Center(child: Text(label).titleMedium),
            ],
          ),
        ),
        const Gap.space16(),
        Wrap(
          alignment: WrapAlignment.center,
          children: [
            for (var idx = 0; idx < _labels.length; idx++)
              Padding(
                padding: const EdgeInsets.symmetric(
                  horizontal: CoreSpace.space4,
                ),
                child: Button(
                  variant: idx == _index
                      ? CoreButtonVariant.primary
                      : CoreButtonVariant.outline,
                  onPressed: () => setState(() => _index = idx),
                  child: Text(_labels[idx]),
                ),
              ),
          ],
        ),
      ],
    );
  }
}

사용 시기 (When to Use)#

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

  • 온보딩 단계처럼 순차적 뷰 사이를 슬라이드 전환할 때
  • 목록/그리드 뷰 모드 전환에 부드러운 애니메이션이 필요할 때
  • 드래그로 인접 뷰를 스크럽하는 인터랙션이 필요할 때

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

  • Tabs: 라벨 기반의 콘텐츠 섹션 전환에
  • Carousel: 자동 재생 / 다수 아이템 슬라이드쇼에
  • Swiper: 카드 스택 형태의 스와이프 인터랙션에

기본 사용법 (Basic Usage)#

Switcher(
  index: _index,
  direction: CoreSwitcherDirection.left,
  onIndexChanged: (next) => setState(() => _index = next),
  items: const [
    Text('Page 1'),
    Text('Page 2'),
    Text('Page 3'),
  ],
)
Switcher(
  index: _index,
  direction: CoreSwitcherDirection.left,
  onIndexChanged: (next) => setState(() => _index = next),
  items: [
    div([text('Page 1')]),
    div([text('Page 2')]),
    div([text('Page 3')]),
  ],
)

Props / Parameters#

Switcher#

속성타입기본값설명
itemsList<W>필수전환 대상 자식 뷰 목록
indexint0현재 보이는 자식 인덱스
direction CoreSwitcherDirection left 새 뷰가 슬라이드해 들어오는 방향
gestureEnabled bool true 드래그 / 스와이프 내비게이션 활성화 여부
onIndexChanged void Function(int)? null 드래그가 다른 뷰에 안착했을 때 새 인덱스로 호출
switcherStyle CoreSwitcherStyle? null 인스턴스별 전환 스타일

CoreSwitcherDirectionup / down / left / right 네 방향을 지원합니다.

스타일 시스템 (Style System)#

Switcher의 전환 chrome은 단일 CoreSwitcherStyle 슬롯으로 흐릅니다 (Epic #1415 option D).

시맨틱 vs 스타일#

  • 시맨틱 / behaviour: 위젯 파라미터로 직접 (index, direction, gestureEnabled, items)
  • chrome / dimensional: switcherStyle: CoreSwitcherStyle? 한 곳으로

Resolve chain#

CoreSwitcherStyle.defaultDuration                // 디자인 시스템 기본값 (300ms)
  → CoreSwitcherTheme.style                       // 프로젝트 공통
  → widget.switcherStyle                          // 인스턴스별

CoreSwitcherStyle 필드#

필드타입적용 영역
durationDuration?슬라이드 / 페이드 전환 시간
Switcher(
  switcherStyle: const CoreSwitcherStyle(
    duration: Duration(milliseconds: 200),
  ),
  items: const [Text('A'), Text('B')],
)

변형 (Variants)#

슬라이드 방향#

direction으로 새 뷰가 들어오는 방향을 지정합니다.

Switcher(direction: CoreSwitcherDirection.up, items: [...])
Switcher(direction: CoreSwitcherDirection.right, items: [...])

드래그 비활성화#

gestureEnabled: false로 드래그를 막고 프로그래매틱 전환만 허용합니다.

Switcher(gestureEnabled: false, items: [...])

동작 스펙 (Behavior)#

인터랙션#

  • 프로그래매틱 전환: index 변경 시 새 뷰가 direction 방향으로 슬라이드
  • 드래그: gestureEnabledtrue면 마우스 / 터치 드래그로 인접 뷰 스크럽
  • 스냅: 드래그 해제 시 가장 가까운 뷰로 스냅하고 onIndexChanged 호출

애니메이션#

  • 슬라이드 + 페이드 전환: 기본 300ms (CoreDuration.ms300), ease-in-out
  • 드래그 중에는 전환이 비활성화되어 포인터를 1:1로 따라감

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

✅ Do#

onIndexChanged로 드래그 결과를 상위 상태에 반영

Switcher(
  index: _index,
  onIndexChanged: (next) => setState(() => _index = next),
  items: const [Text('A'), Text('B')],
)

드래그가 안착한 인덱스를 상태에 반영해야 다음 빌드에서 일관된 뷰가 보인다.


❌ Don't#

라벨 기반 콘텐츠 섹션 전환에 Switcher 사용

// ❌ 라벨 탭 UI 가 필요한 경우
Switcher(
  items: const [Text('Account'), Text('Password')],
)

라벨 탭이 필요하면 Tabs가 적합하다. Switcher 는 슬라이드 전환이 핵심이다.

접근성 (Accessibility)#

  • 비활성 뷰는 aria-hidden="true" (Web) 로 표시
  • 루트 컨테이너는 role="group" (Web)

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

Switcher는 Flutter와 Web이 동일한 items / index / direction / gestureEnabled / onIndexChanged / switcherStyle API와 동작을 갖습니다. Flutter는 커스텀 RenderObject로 자식 크기를 보간하고, Web은 CSS transform 트랙 슬라이드로 동일한 시각 결과를 냅니다. 전환 곡선은 양쪽 모두 ease-in-out입니다.

  • Tabs: 라벨 기반 콘텐츠 섹션 전환
  • Carousel: 자동 재생 슬라이드쇼
  • Swiper: 카드 스택 스와이프