Toggle | CoUI
LogoCoUI

Toggle

토글 스위치 컴포넌트

Toggle#

켜기/끄기 상태를 전환하는 토글 스위치 컴포넌트입니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 즉시 적용되는 on/off 설정을 전환할 때 (다크 모드, 알림 등)
  • 두 가지 상태만 있고 변경이 바로 반영될 때
  • 설정 목록에서 각 옵션을 개별적으로 토글할 때

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

  • Checkbox: 폼 제출 후에 반영되는 선택일 때
  • Select: 두 가지 이상의 옵션 중 선택할 때
  • Button: 단순 액션 트리거일 때 (상태 보존 불필요)

기본 사용법 (Basic Usage)#

// 기본 토글
Toggle(
  value: isDarkMode,
  onChanged: handleDarkModeToggle,
)

// 라벨 텍스트
Toggle(
  value: isNotificationEnabled,
  onChanged: handleNotificationToggle,
  label: '알림 수신',
)

// 앞에 라벨 Widget 배치
Toggle(
  value: isActive,
  onChanged: handleToggle,
  prefix: Text('자동 저장'),
)

// 뒤에 라벨 Widget 배치
Toggle(
  value: isActive,
  onChanged: handleToggle,
  suffix: Text('자동 저장'),
)

// 비활성화
Toggle(
  value: false,
  onChanged: null,
  enabled: false,
  label: '비활성 옵션',
)
// 기본 토글
Toggle(
  value: isDarkMode,
  onChanged: handleDarkModeToggle,
)

// 라벨 텍스트
Toggle(
  value: isNotificationEnabled,
  onChanged: handleNotificationToggle,
  label: '알림 수신',
)

// 비활성화
Toggle(
  value: false,
  enabled: false,
)

// 초기 상태 on
Toggle(
  value: true,
  onChanged: handleToggle,
)

Props / Parameters#

속성타입기본값설명
value bool Flutter 필수 / Web false 현재 상태
onChanged ValueChanged<bool>? / CoreValueChanged<bool>? null (Flutter 는 명시 전달 필수) 상태 변경 콜백 — null 이면 비활성
labelString?null라벨 텍스트
variant CoreToggleVariant CoreToggleVariant.defaultVariant 시맨틱 변형 (위젯 파라미터)
size CoreComponentSize CoreComponentSize.md 크기 토큰 (위젯 파라미터)
enabled bool? nullonChanged != null 로 유도 활성화 여부
prefix Widget? / Component? null 토글 앞 위젯
suffix Widget? / Component? null 토글 뒤 위젯
toggleStyle CoreToggleStyle? null track + thumb chrome / nested slot 묶음
nameString?null폼 제출 이름

Flutter 전용 런타임 인프라 (Web 은 브라우저 native focus / DOM 이 같은 capability 를 제공):

속성타입기본값설명
controller ToggleController? null (Flutter 전용) 외부 ValueNotifier<bool> 기반 상태 컨트롤러 (toggle() 제공)
focusNode FocusNode? null (Flutter 전용) 외부 포커스 노드
statesController WidgetStatesController? null (Flutter 전용) 외부 위젯 상태 컨트롤러

빠른 오버라이드 (Chain)#

이미 만든 Toggle 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class ToggleChainExample extends StatefulWidget {
  const ToggleChainExample({super.key});

  @override
  State<ToggleChainExample> createState() => _ToggleChainExampleState();
}

class _ToggleChainExampleState extends State<ToggleChainExample> {
  bool _value = false;

  @override
  Widget build(BuildContext context) {
    return Toggle(
      value: _value,
      onChanged: (v) => setState(() => _value = v),
      label: 'Toggle',
    ).withStyle(
      const CoreToggleStyle(
        trackActiveColor: CoreColor.token(CoreColors.success),
        trackInactiveColor: CoreColor.token(CoreColors.outlineVariant),
        trackBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
        thumbBorderRadius: CoreBorderRadius.all(CoreRadius.radius2),
        iconLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
      ),
    );
  }
}
class ToggleChainExample extends StatefulComponent {
  const ToggleChainExample({super.key});

  @override
  State<ToggleChainExample> createState() => _ToggleChainExampleState();
}

class _ToggleChainExampleState extends State<ToggleChainExample> {
  bool _value = false;

  @override
  Component build(BuildContext context) {
    return Toggle(
      value: _value,
      onChanged: (v) => setState(() => _value = v),
      label: 'Toggle',
    ).withStyle(
      const CoreToggleStyle(
        trackActiveColor: CoreColor.token(CoreColors.success),
        trackInactiveColor: CoreColor.token(CoreColors.outlineVariant),
        trackBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
        thumbBorderRadius: CoreBorderRadius.all(CoreRadius.radius2),
        iconLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
      ),
    );
  }
}

스타일 시스템 — toggleStyle#

Toggle 의 track + thumb chrome / 슬롯 미세 조정은 단일 toggleStyle (CoreToggleStyle) 으로 흐릅니다. 시맨틱 enum (variant, size) 은 위젯 파라미터.

Toggle(
  variant: CoreToggleVariant.defaultVariant,
  size: CoreComponentSize.md,
  value: enabled,
  onChanged: handleChange,
  label: '알림 받기',
  toggleStyle: CoreToggleStyle(
    trackActiveColor: CoreColor.token(CoreColors.primary),
    trackInactiveColor: CoreColor.token(CoreColors.outline),
    thumbActiveColor: CoreColor.token(CoreColors.onPrimary),
    thumbInactiveColor: CoreColor.token(CoreColors.onSurface),
    trackBorderRadius: CoreBorderRadius.all(CoreRadius.radius12),
    iconLabelGapStyle: CoreGapStyle(size: CoreSpace.space8),
    labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
  ),
)

CoreToggleStyle 필드#

필드타입설명
trackActiveColor CoreColor? Track fill colour when on.
trackInactiveColor CoreColor? Track fill colour when off.
trackBorderRadius CoreBorderRadius? Track border radius override.
trackWidth double? Track width override (logical px).
trackHeight double? Track height override (logical px).
thumbActiveColor CoreColor? Thumb fill colour when on.
thumbInactiveColor CoreColor? Thumb fill colour when off.
thumbSize double? Thumb size (width = height) override (logical px).
thumbBorderRadius CoreBorderRadius? Thumb border radius override. Carries no defaultThumbBorderRadius , and the two platforms key different things on its absence — a constant would break both. Flutter tests resolved.thumbBorderRadius == null to choose BoxShape.circle ; a BoxDecoration cannot hold a circle shape and a radius at once, so any non-null default permanently moves the thumb onto the BoxShape.rectangle branch. Web falls back to the resolved trackBorderRadius , so an unset thumb radius follows the track — including a per-instance trackBorderRadius override. A constant here would freeze the thumb and silently unpair it from the track.
thumbOffset double? Thumb travel offset when on (logical px). null falls back to [defaultsBySize] for the widget's size.
inactiveThumbOffset double? Thumb resting (off-state) X translation (logical px). null falls back to [defaultInactiveThumbOffset] (0 — flush with the track's leading edge).
padding CoreEdgeInsets? Inner padding inside the track. null falls back to [defaultsBySize] for the widget's size.
iconLabelGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between the toggle track and the prefix / suffix / label slots — forwarded straight to the Gap widget that separates the slots. null defers to the per-size default in [defaultsBySize], else [defaultIconLabelGapStyle].
colorTransitionDuration Duration? Track colour transition duration. null falls back to [defaultColorTransitionDuration].
thumbTransitionDuration Duration? Thumb travel transition duration. null falls back to [defaultThumbTransitionDuration].
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed Clickable (press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded.
labelStyle CoreTextStyle? Label text style override.

CoreToggleStyle 변형별 기본값 (CoreToggleVariantStyle)#

필드defaultVariant
activeTrackprimary
inactiveTracksurfaceContainer
activeThumbsurface
inactiveThumbonSurface
disabledTracksurfaceContainerHigh
disabledThumbsurface
disabledForegroundColoronSurfaceVariant

variant 별 색은 CoreToggleVariantStyle (activeTrack / inactiveTrack / activeThumb / inactiveThumb / disabledTrack / disabledThumb / disabledForeground) 로 CoreToggleStyle.defaultsByVariant 에서, 크기는 CoreToggleStyle.defaultsBySize 에서 결정됩니다.

Resolve chain#

design system default
  → CoreToggleTheme.style                      // 프로젝트 공통
  → parent component slot override
  → widget.toggleStyle                         // 인스턴스별

nested labelStyle 은 자기 컴포넌트의 resolve chain 으로 다시 한 번 머지됩니다.

변형 (Variants)#

크기#

Toggle(value: v1, onChanged: handle, size: CoreComponentSize.sm)
Toggle(value: v2, onChanged: handle, size: CoreComponentSize.md)
Toggle(value: v3, onChanged: handle, size: CoreComponentSize.lg)

설정 목록에서 사용#

Column(
  children: [
    Toggle(
      value: settings.darkMode,
      onChanged: handleDarkMode,
      label: '다크 모드',
    ),
    Toggle(
      value: settings.notifications,
      onChanged: handleNotifications,
      label: '푸시 알림',
    ),
    Toggle(
      value: settings.autoSave,
      onChanged: handleAutoSave,
      label: '자동 저장',
    ),
  ],
)

동작 스펙 (Behavior)#

상태 전환#

  • 클릭/탭 시 valuetruefalse 토글
  • enabled: false(또는 onChanged: null)면 인터랙션 비활성 + disabled 색 토큰 적용

상태 관리#

// 직접 콜백 방식
Toggle(
  value: isDarkMode,
  onChanged: (value) => setState(() => isDarkMode = value),
)

애니메이션#

  • Flutter: 100ms, Curves.easeInOut
    • 트랙 색상: AnimatedContainer로 활성/비활성 색상 전환
    • 썸네일 이동: AnimatedPositioned로 좌우 슬라이딩
  • Web: CSS transition-transform으로 썸네일 위치 전환

크기 토큰#

트랙 높이는 6단계 사다리(12/16/20/24/28/32 — Switch/Checkbox/Radio 세 컨트롤이 공유하는 값, epic-k-dimension-table.md §축4)를 따른다. Track 너비와 Thumb 크기는 트랙 높이의 파생값이다: 너비 = 높이 × 1.625, Thumb = 높이 − 2.

SizeTrack (W×H)ThumbFlutterWeb
xs 19.5×12 10 CoreToggleStyle.defaultsBySize Tailwind
sm 26×16 14 CoreToggleStyle.defaultsBySize Tailwind
md 32.5×20 18 CoreToggleStyle.defaultsBySize Tailwind
lg 39×24 22 CoreToggleStyle.defaultsBySize Tailwind
xl 45.5×28 26 CoreToggleStyle.defaultsBySize Tailwind
2xl 52×32 30 CoreToggleStyle.defaultsBySize Tailwind

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

✅ Do#

토글에 항상 라벨을 제공하세요.

Toggle(
  value: isDarkMode,
  onChanged: handleDarkMode,
  label: '다크 모드',
)

토글의 목적이 명확해집니다.


❌ Don't#

라벨 없이 토글만 배치하지 마세요.

Toggle(
  value: isDarkMode,
  onChanged: handleDarkMode,
)

사용자가 무엇을 켜고 끄는지 알 수 없습니다.

✅ Do#

즉시 적용되는 설정에 토글을 사용하세요.

Toggle(
  value: notifications,
  onChanged: (value) {
    updateNotificationSetting(value); // 즉시 서버 반영
  },
  label: '푸시 알림',
)

토글 변경 = 즉시 적용이 사용자의 기대입니다.


❌ Don't#

"저장" 버튼이 필요한 설정에 토글을 사용하지 마세요.

저장 전까지 실제 반영이 안 되면 사용자가 혼란스러워합니다. 이 경우 Checkbox가 더 적합합니다.

✅ Do#

긍정형 라벨로 on 상태를 명확히 하세요.

Toggle(label: '알림 받기', ...)
Toggle(label: '자동 저장', ...)

on = 활성화가 직관적입니다.


❌ Don't#

부정형 라벨을 사용하지 마세요.

Toggle(label: '알림 끄기', ...)

켜면 "알림 끄기"가 활성화되어 이중 부정이 됩니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Space토글 전환
Enter토글 전환
Tab다음 요소로 포커스 이동

스크린 리더#

  • Flutter: Clickable 위젯으로 플랫폼 시맨틱 전달. 포커스 시 라벨과 상태(켜짐/꺼짐) 읽힘
  • Web: role="switch" + aria-checked="true|false"로 스위치 역할 명시. 스크린 리더가 "스위치, 켜짐" 형태로 읽음

포커스 표시#

  • Flutter: Clickable 위젯의 포커스 링
  • Web: focus-visible:ring-2 focus-visible:ring-ring CSS 포커스 링

비활성 상태#

양 플랫폼 모두 불투명도가 아니라 전용 disabled 색 토큰(CoreToggleVariantStyledisabledTrack / disabledThumb / disabledForeground)으로 표현합니다 — 색 강제 모드에서도 비활성이 활성처럼 읽히지 않습니다.

  • Flutter: 트랙 / thumb / 라벨 색을 disabled 토큰으로 교체 + 인터랙션 차단
  • Web: 같은 disabled 토큰 class + hidden <input>disabled 속성

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

Toggle은 coui_coreCoreToggleVariantStyleCoreToggleStyle.defaultsBySize를 사용하여 Flutter/Web 동일한 디자인 토큰 기반으로 렌더링됩니다.

항목FlutterWeb
애니메이션 100ms easeInOut (AnimatedPositioned) CSS transition-transform
크기 토큰 CoreToggleStyle.defaultsBySize (pixel) CoreToggleStyle.defaultsBySize (Tailwind 어댑터)
색상 토큰 CoreToggleStyle.defaultsByVariant (CoreToggleVariantStyle) CoreToggleStyle.defaultsByVariant (CoreToggleVariantStyle)
런타임 인프라 controller / focusNode / statesController 브라우저 native focus + hidden <input>
ARIAFlutter 플랫폼 시맨틱role="switch", aria-checked
폼 연동FormValueSupplier 믹스인hidden <input>
  • Checkbox: 체크박스. "제출 후 적용"되는 선택에 적합 (Toggle은 즉시 적용)
  • Slider: 범위 값 조절. boolean이 아닌 연속 값일 때
  • Form: 폼 컨테이너. Toggle을 폼 필드로 등록 가능

조합 예제#

// 설정 페이지 패턴
Drawer(
  title: '설정',
  content: Column(children: [
    Toggle(
      value: settings.darkMode,
      onChanged: handleDarkMode,
      label: '다크 모드',
    ),
    Toggle(
      value: settings.notifications,
      onChanged: handleNotifications,
      label: '푸시 알림',
    ),
    Toggle(
      value: settings.autoSave,
      onChanged: handleAutoSave,
      label: '자동 저장',
    ),
    Toggle(
      value: settings.analytics,
      onChanged: handleAnalytics,
      label: '사용 통계 수집',
      enabled: !settings.isGuest,
    ),
  ]),
)