Toggle#
켜기/끄기 상태를 전환하는 토글 스위치 컴포넌트입니다.
Live Preview#
class ToggleDefaultExample extends StatefulComponent {
const ToggleDefaultExample({super.key});
@override
State<ToggleDefaultExample> createState() => _ToggleDefaultExampleState();
}
class _ToggleDefaultExampleState extends State<ToggleDefaultExample> {
bool _value = false;
@override
Component build(BuildContext context) {
return Toggle(
value: _value,
label: 'Toggle',
onChanged: (v) => setState(() => _value = v),
);
}
}
class ToggleDefaultExample extends StatefulWidget {
const ToggleDefaultExample({super.key});
@override
State<ToggleDefaultExample> createState() => _ToggleDefaultExampleState();
}
class _ToggleDefaultExampleState extends State<ToggleDefaultExample> {
bool _value = false;
@override
Widget build(BuildContext context) {
return Toggle(
value: _value,
onChanged: (v) => setState(() => _value = v),
label: 'Toggle',
);
}
}
class ToggleOnExample extends StatefulComponent {
const ToggleOnExample({super.key});
@override
State<ToggleOnExample> createState() => _ToggleOnExampleState();
}
class _ToggleOnExampleState extends State<ToggleOnExample> {
bool _value = true;
@override
Component build(BuildContext context) {
return Toggle(
value: _value,
label: 'Toggle',
onChanged: (v) => setState(() => _value = v),
);
}
}
class ToggleOnExample extends StatefulWidget {
const ToggleOnExample({super.key});
@override
State<ToggleOnExample> createState() => _ToggleOnExampleState();
}
class _ToggleOnExampleState extends State<ToggleOnExample> {
bool _value = true;
@override
Widget build(BuildContext context) {
return Toggle(
value: _value,
onChanged: (v) => setState(() => _value = v),
label: 'Toggle',
);
}
}
class ToggleDisabledExample extends StatefulComponent {
const ToggleDisabledExample({super.key});
@override
State<ToggleDisabledExample> createState() => _ToggleDisabledExampleState();
}
class _ToggleDisabledExampleState extends State<ToggleDisabledExample> {
bool _value = false;
@override
Component build(BuildContext context) {
return Toggle(
value: _value,
label: 'Toggle',
enabled: false,
onChanged: (value) {
setState(() {
_value = value;
});
},
);
}
}
class ToggleDisabledExample extends StatefulWidget {
const ToggleDisabledExample({super.key});
@override
State<ToggleDisabledExample> createState() => _ToggleDisabledExampleState();
}
class _ToggleDisabledExampleState extends State<ToggleDisabledExample> {
bool _value = false;
@override
Widget build(BuildContext context) {
return Toggle(
value: _value,
enabled: false,
label: 'Toggle',
onChanged: (value) {
setState(() {
_value = value;
});
},
);
}
}
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),
),
);
}
}
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),
),
);
}
}
사용 시기 (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 이면 비활성 |
label | String? | null | 라벨 텍스트 |
variant |
CoreToggleVariant |
CoreToggleVariant.defaultVariant |
시맨틱 변형 (위젯 파라미터) |
size |
CoreComponentSize |
CoreComponentSize.md |
크기 토큰 (위젯 파라미터) |
enabled |
bool? |
null → onChanged != null 로 유도 |
활성화 여부 |
prefix |
Widget? / Component? |
null |
토글 앞 위젯 |
suffix |
Widget? / Component? |
null |
토글 뒤 위젯 |
toggleStyle |
CoreToggleStyle? |
null |
track + thumb chrome / nested slot 묶음 |
name | String? | 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 |
|---|---|
activeTrack | primary |
inactiveTrack | surfaceContainer |
activeThumb | surface |
inactiveThumb | onSurface |
disabledTrack | surfaceContainerHigh |
disabledThumb | surface |
disabledForegroundColor | onSurfaceVariant |
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)#
상태 전환#
- 클릭/탭 시
value가true↔false토글 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.
| Size | Track (W×H) | Thumb | Flutter | Web |
|---|---|---|---|---|
| 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-ringCSS 포커스 링
비활성 상태#
양 플랫폼 모두 불투명도가 아니라 전용 disabled 색 토큰(CoreToggleVariantStyle 의 disabledTrack /
disabledThumb / disabledForeground)으로 표현합니다 — 색 강제 모드에서도 비활성이 활성처럼 읽히지 않습니다.
- Flutter: 트랙 / thumb / 라벨 색을 disabled 토큰으로 교체 + 인터랙션 차단
-
Web: 같은 disabled 토큰 class + hidden
<input>의disabled속성
크로스 플랫폼 차이점 (Platform Differences)#
Toggle은
coui_core의CoreToggleVariantStyle과CoreToggleStyle.defaultsBySize를 사용하여 Flutter/Web 동일한 디자인 토큰 기반으로 렌더링됩니다.
| 항목 | Flutter | Web |
|---|---|---|
| 애니메이션 | 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> |
| ARIA | Flutter 플랫폼 시맨틱 | role="switch", aria-checked |
| 폼 연동 | FormValueSupplier 믹스인 | hidden <input> |
관련 컴포넌트 (Related Components)#
- 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,
),
]),
)