Toggle
토글 스위치 컴포넌트
목차
켜기/끄기 상태를 전환하는 토글 스위치 컴포넌트입니다.
기본 사용법 (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,
)
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',
);
}
}
구조 (Anatomy)#
코드가 정본입니다. 부분 이름은 위젯 슬롯과 같고, Figma ❖ Toggle
레이어와도 맞습니다.
| 부분 | 역할 | 슬롯 |
|---|---|---|
prefix | 토글 앞 | prefix |
suffix | 토글 뒤 | suffix |
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 즉시 적용되는 on/off 설정을 전환할 때 (다크 모드, 알림 등)
- 두 가지 상태만 있고 변경이 바로 반영될 때
- 설정 목록에서 각 옵션을 개별적으로 토글할 때
대신 다른 컴포넌트를 사용하세요:
Checkbox: 폼 제출 후에 반영되는 선택일 때Select: 두 가지 이상의 옵션 중 선택할 때Button: 단순 액션 트리거일 때 (상태 보존 불필요)
Props / Parameters#
W는 Flutter의 Widget, Web의 Component를 뜻하는 prefix/suffix 슬롯 타입입니다. 콜백 typedef는 두 플랫폼 모두
void Function(bool) 의미입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value |
bool |
명시 (Flutter 필수) | 현재 상태. 양쪽 호출에서 명시적으로 전달 |
onChanged |
ValueChanged<bool>? / CoreValueChanged<bool>? |
null (Flutter 는 명시 전달 필수) |
상태 변경 콜백 — null 이면 비활성 |
label | String? | null | 라벨 텍스트 |
labelMaxLines |
int? |
null |
라벨이 넘칠 때까지 허용하는 줄 수. null 은 현행대로 줄 수를 제한하지 않습니다 |
labelOverflow |
CoreTextOverflow? |
null |
labelMaxLines
에 도달한 라벨의 넘침 처리 (
clip
/
fade
/
ellipsis
/
visible
)
|
variant |
CoreToggleVariant |
CoreToggleVariant.defaultVariant |
시맨틱 변형 (위젯 파라미터) |
size |
CoreComponentSize |
CoreComponentSize.md |
크기 토큰 (위젯 파라미터). 사다리는
sm
~
xl2
—
xs
는 행이 없어
sm
으로 그려지며, 예고 중입니다(표 아래 안내)
|
enabled |
bool? |
null → onChanged != null 로 유도 |
활성화 여부 |
prefix | W? | null | 토글 앞 콘텐츠 |
suffix | W? | null | 토글 뒤 콘텐츠 |
toggleStyle |
CoreToggleStyle? |
null |
track + thumb chrome / nested slot 묶음 |
name | String? | null | 폼 제출 이름 |
xs는 지금도sm으로 그려집니다 — 대신sm을 쓰세요. 화면은 바뀌지 않습니다. 지금은 assert 가 켜진 실행(디버그 빌드, Jaspr 정적 빌드의 사전 렌더)에서 로그 경고가 한 번 나오고 — 경고일 뿐 화면이나 빌드를 멈추지 않습니다 — 이르면 coui_core 0.153 부터는 assert 가 켜진 실행이xs에서 멈춥니다. assert 를 빼는 빌드는 계속sm으로 그립니다.
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),
thumbShadow: CoreShadow.sm,
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),
thumbShadow: CoreShadow.sm,
iconLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
),
);
}
}
스타일 시스템 (Style System)#
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 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
thumbShadow |
List<CoreShadowLayer>? |
Shadow layers on the moving thumb, independent of the track and lens. |
thumbShadowBaseColor |
CoreColor? |
Base tint of [thumbShadow]; layer opacity and dark strength are retained. |
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]. |
stateLayer |
CoreStateLayer? |
How strongly the track's own ink is laid over its fill while hovered / pressed.
null
falls back to [defaultStateLayer].
|
stateLayerActiveColor |
CoreColor? |
The ink the state layer paints over the on track.
null
falls back to the variant's
activeStateLayer
.
|
stateLayerInactiveColor |
CoreColor? |
The ink the state layer paints over the off track.
null
falls back to the variant's
inactiveStateLayer
.
|
focusOutlineStyle |
CoreFocusOutlineStyle? |
Nested [CoreFocusOutlineStyle] slot for the focus outline drawn around the track. Merged on top of [defaultFocusOutlineStyle] and raw-forwarded to
FocusOutline
.
|
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed
Clickable
(press scale / durations / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded.
|
labelStyle |
CoreTextStyle? |
Label text style override — merged on top of [defaultLabelStyle] and the size row's role. |
lensOutset |
CoreEdgeInsets? |
How far the transient lens reaches past the thumb on each side.
null
falls back to [defaultLensOutset]. The lens is a clear capsule that appears over the thumb for exactly the thumb's travel ([thumbTransitionDuration]) and is absent at rest, so this never changes the switch's resting size, layout or pixels. What the capsule shows — blur, refraction, sheen — is the material the active surface style gives a floating surface; under a style that gives none, the lens draws nothing at all. Read as authored under every density, a stated value included — see [defaultLensOutset] for why this inset is geometry, not spacing.
|
lensContainerStyle |
CoreOutlinedContainerStyle? |
Nested [CoreOutlinedContainerStyle] slot for the transient lens's chrome — merged on top of [defaultLensContainerStyle] and raw-forwarded to the lens (Flutter composes
OutlinedContainer
with it; Web writes its fields inline on the lens element). The slot's
width
/
height
are not read: the lens's size is the thumb grown by [lensOutset], and both resolvers lay that size over the slot.
|
lensFadeFraction |
double? |
Share of the thumb's travel the lens spends fading in, and again fading out.
null
falls back to [defaultLensFadeFraction]; a value below
0
or above [maxLensFadeFraction] is clamped to that bound.
0
shows the lens at full strength for the whole travel; [maxLensFadeFraction] fades it straight in and straight out with no hold.
|
CoreToggleStyle 변형별 기본값 (CoreToggleVariantStyle)#
| 필드 | defaultVariant |
|---|---|
activeTrack | primary |
inactiveTrack | surfaceContainerHighest |
activeThumb | onPrimary |
inactiveThumb | onSurface |
disabledActiveTrack | disabledContent |
disabledInactiveTrack | disabledContainer |
disabledThumb | surface |
disabledInactiveThumb | disabledContent |
disabledForegroundColor | disabledContent |
activeStateLayer | surface |
inactiveStateLayer | onSurface |
variant 별 색은 CoreToggleVariantStyle (activeTrack / inactiveTrack
/ activeThumb / inactiveThumb / disabledActiveTrack / disabledInactiveTrack
/ disabledThumb / disabledInactiveThumb / disabledForegroundColor
/ activeStateLayer / inactiveStateLayer) 로 CoreToggleStyle.defaultsByVariant
에서, 크기·라벨 role 은 CoreToggleStyle.defaultsBySize 에서 결정됩니다. hover / pressed 는 트랙 위에 자기 ink 를
stateLayer 램프(8% / 16%)로 덮은 wash 이고, 키보드 포커스는 트랙만 감싸는 focusOutlineStyle 링입니다.
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),
)
상태 레이어 (hover / pressed)#
-
포인터가 올라오면 트랙 위에 트랙 자신의 ink(
activeStateLayer=surface/inactiveStateLayer=onSurface)를stateLayer.hover(8%) 로, 누르는 동안stateLayer.pressed(16%) 로 덮습니다. 램프는CoreStateLayer.standard. -
누르는 동안의 피드백은 트랙의 16% wash 와
Clickable의 눌림 축소(기본값) 둘 다입니다 — 축소는 제스처 동안만 존재하는 모션이라 Figma 캔버스의pressed포즈에는 wash 만 그려집니다. -
Flutter:
Clickable이 hover / press 를 보고하고 위젯이 미리 합성된OutlinedContainer번들 중 하나를 고릅니다. -
Web:
.scope:hover .co-toggle-track/:active스코프 규칙이color-mix()로 같은 wash 를 그립니다.
애니메이션#
-
Flutter: 100ms,
Curves.easeInOut- 트랙 색상:
OutlinedContainer로 활성/비활성 색상 전환 - 썸네일 이동:
AnimatedPositioned로 좌우 슬라이딩
- 트랙 색상:
- Web: CSS
transition-transform으로 썸네일 위치 전환
전환 렌즈#
사용자가 스위치를 직접 켜고 끌 때(탭 · 클릭 · 키보드 Space/Enter) 썸이 움직이는 동안에만 썸보다 큰 투명 캡슐(렌즈)이 썸을 따라 이동합니다. 앱이 코드로 값을 바꿀 때(폼 리셋 · '모두 켜기' 처럼) 썸은 그대로 움직이지만 렌즈는 뜨지 않습니다. 정지 상태에는 렌즈가 트리·DOM 에 아예 없으므로, 멈춰 있는 스위치의 크기·레이아웃·픽셀은 렌즈가 없을 때와 같습니다.
-
모양: 썸 상자를
lensOutset만큼 키운 알약. 레이아웃을 차지하지 않아 트랙 밖으로 넘칠 수 있습니다. -
크롬:
lensContainerStyle슬롯(CoreOutlinedContainerStyle) — 기본은 채움 · 테두리 · 그림자가 없는 알약(defaultLensContainerStyle)입니다. 크기는 슬롯이 아니라lensOutset이 정합니다. -
재질: 렌즈는 떠 있는 표면(
surface:functional)이고 기본 크롬에 채움이 없습니다. 보이는 것 (블러·굴절·가장자리 띠)은 전부 활성 서피스 스타일이 떠 있는 표면에 주는 재질이며, 재질이 없는 스타일에서는 아무것도 그려지지 않습니다. 카드처럼 유리 표면 안에 놓인 스위치에서는 블러·굴절이 빠지고 가장자리 띠만 보입니다. -
시간: 썸 이동과 같은
thumbTransitionDuration동안 살고, 처음lensFadeFraction(기본 1/4)에 나타나 마지막 같은 몫에 사라집니다. 전환 tier 이므로 동작 줄이기에서는 뜨지 않습니다. - 비활성: 비활성 토글은 재질 표면으로 읽히지 않으므로 렌즈가 없습니다.
- 캔버스: Figma Toggle 세트에는 전환 중 자세가 없습니다 — 전환은 프리뷰와 실기기에서 확인합니다.
크기 토큰#
트랙은 5단계 사다리(높이 16/20/24/28/32)의 pill 이고, 너비는 높이 + 16 입니다
(32/36/40/44/48). thumb 은 높이 − 4(안쪽 여백 space/2
양쪽) — On/Off 에서 같은
크기이고, 움직이는 거리는 모든 크기에서 16 입니다. 라벨은 크기마다
역할을 고르고(bodySmall / bodyMedium / bodyLarge
/ bodyLarge /
bodyLarge — 13 / 14 / 15 / 15 / 15), 굵기 400 · 행간 150% 는 역할의
것입니다. xs 행은 없습니다 — 24×12 트랙의 8px 이동은 피드백으로 읽히지 않아 사다리에서 제거됐고,
CoreComponentSize.xs 를 넘기면 sm 으로 그려집니다. 이 xs
는 예고 중이라
이르면 coui_core 0.153 부터 assert 가 켜진 실행이 멈춥니다 — 위 속성 표 아래 안내를 보세요.
| Size | Track (W×H) | Thumb | 이동 | gap | 라벨 | Flutter | Web |
|---|---|---|---|---|---|---|---|
| sm | 32×16 | 12 | 16 | 6 | 13 | CoreToggleStyle.defaultsBySize |
inline rem |
| md | 36×20 | 16 | 16 | 8 | 14 | CoreToggleStyle.defaultsBySize |
inline rem |
| lg | 40×24 | 20 | 16 | 8 | 15 | CoreToggleStyle.defaultsBySize |
inline rem |
| xl | 44×28 | 24 | 16 | 10 | 15 | CoreToggleStyle.defaultsBySize |
inline rem |
| xl2 | 48×32 | 28 | 16 | 10 | 15 | CoreToggleStyle.defaultsBySize |
inline rem |
라벨이 달린 Toggle 의 행 높이는 라벨 줄 상자(라벨 역할의 행간 150%)와 트랙 중 큰 쪽입니다. md 는 라벨 줄 상자 14 × 1.5 = 21 이 트랙 20 보다 커서 행이 21 이고(sm
도 13 × 1.5 = 19.5 가 트랙 16 보다 큽니다), lg 부터는 라벨 줄 상자(15 × 1.5 = 22.5)가 트랙 24 보다 작아 트랙이 이기므로 행 높이가 트랙 높이와 같습니다.
크기를 적지 않은 Toggle(기본 md)은 너비만 40 → 36 으로 좁아지고 높이는 그대로입니다.
이전 너비가 필요한 앱은 toggleStyle: CoreToggleStyle(trackWidth: 40, thumbOffset: 20)
으로
되돌릴 수 있습니다.
사용 가이드라인 (Usage Guidelines)#
하나의 bool 값을 트랙과 thumb으로 표현하려면 Toggle을 사용합니다. 라벨·설명을 포함한 전체 설정 행은 SwitchField, 두 아이콘/콘텐츠 교체는 Swap, indeterminate가 필요한 독립 선택은 Checkbox를 선택합니다. enabled를 생략하면 콜백 존재로 활성 상태를 유도하고 값 변경은 호출자가 반영합니다.
✅ 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)#
역할#
-
Flutter —
Semantics(toggled: value)로 켜짐/꺼짐을 내보냅니다. 합성된Clickable에semanticsButton: false를 넘겨 일반 "버튼" 역할은 꺼져 있고,Clickable이 붙이는Semantics(enabled:)는 그대로 남습니다. -
Web — 인터랙션 루트에
role="switch"·aria-checked="true" | "false"·tabindex가 붙고, 비활성이면aria-disabled="true"가 더해집니다. 트랙과 thumb 은aria-hidden="true"라 접근성 트리에 들어가지 않습니다 — 역할과 상태를 나르는 노드는 루트 하나뿐입니다.
name 을 주면 폼 제출용 히든 <input type="checkbox"> 가 함께 나가지만 sr-only
· tabindex="-1" · aria-hidden="true" 라 역시 접근성 트리 밖입니다.
키보드#
| 키 | Flutter | Web |
|---|---|---|
Space | 토글 전환 | 토글 전환 |
Enter | 토글 전환 | 토글 전환 |
Tab | 다음 포커스 가능 요소로 | 다음 포커스 가능 요소로 |
Shift+Tab | 이전 포커스 가능 요소로 | 이전 포커스 가능 요소로 |
활성화 키는 양쪽 다 합성된 Clickable 이 처리합니다. 화살표로 켜고 끄는 조작은 양 플랫폼 모두 없습니다.
비활성 토글은 양 플랫폼 모두 Tab 순회에서 빠집니다 — Flutter 는 FocusableActionDetector(enabled: false), Web 은
tabindex="-1".
스크린 리더#
접근 가능한 이름은 스위치 노드의 내용에서 옵니다 — label(또는 prefix · suffix)에 넣은 텍스트가 그대로 이름이 됩니다. 셋 다 비우면 이름이 비고, 리더는 "스위치, 켜짐" 처럼 역할과 상태만 읽습니다(아래 알려진 제약 참고).
켜짐/꺼짐은 양쪽 다 통보됩니다 — Flutter Semantics(toggled:), Web aria-checked. 값이 바뀐 사실을 따로 알리는 live region 은 없습니다; 상태는 스위치 노드 자체에 실려 있습니다.
양 플랫폼 모두 불투명도가 아니라 전용 disabled 색 토큰으로 표현합니다 —
비활성은 트랙이 말합니다: On 은 disabledActiveTrack(disabled-content),
Off 는 disabledInactiveTrack(disabled-container), 라벨은
disabledForegroundColor(disabled-content). 활성 Off 트랙은
surface-container-highest 이고 disabled-container
는 그보다 낮은 컨테이너 톤이라,
Off 에서도 활성/비활성은 thumb 과 라벨 색뿐 아니라 트랙 톤으로도 구분됩니다.
활성 thumb 은 자기 트랙에서 읽히는 잉크입니다 — On 은 activeThumb(on-primary),
Off 는 inactiveThumb(on-surface, Off 트랙 surface-container-highest
위). 페이지
surface 를 빌려 쓰지 않으므로 어두운 페이지에서도 Off thumb 이 트랙에서 떨어져 보입니다.
비활성 thumb 은 자세마다 다릅니다. On 은 disabledThumb(surface) — On 트랙
disabled-content 위에서 보입니다. Off 는 disabledInactiveThumb(disabled-content)
— surface thumb 은 disabled-container 트랙 위에서 거의 사라지기 때문입니다(CoUI
팔레트 실측 Light 1.09:1 · Dark 1.26:1). disabled-content 는 그 트랙에서 그래픽
기준 3:1 을 넘습니다.
포커스 관리#
- 진입 — 이동 없음. 오버레이가 아니라 인라인 컨트롤이라 마운트 시 포커스를 옮기지 않습니다.
- 이탈 — 해당 없음.
- 트랩 — 없음.
Tab은 토글을 지나 그대로 빠져나갑니다. -
포커스 표시 — 링은 트랙만 감쌉니다(행 전체가 아님).
투명 간격(2px) + 링(
focus-ring, 2px), 모서리radius/9999이며toggleStyle.focusOutlineStyle로 오버라이드합니다. Flutter 는 트랙을 감싼FocusOutline(Clickable의 행 링은focusOutline: false), Web 은.scope:focus-visible .co-toggle-trackscoped 규칙의outline을 씁니다.
알려진 제약#
-
라벨 없이 쓰면
role="switch"노드에 이름이 붙지 않고, Web 에는 이름을 채울 다른 경로가 없습니다. 트랙과 thumb 은aria-hidden이라 이름은label·prefix·suffix의 내용에서만 오는데, WebToggle은attributes·classes·css를 생성자에서 받지 않습니다 —copyWith가 인자로 받아들이기는 하지만 새 인스턴스로 넘기지 않습니다. Flutter 는 호출부가Semantics(label: ...)로 감싸 이름을 줄 수 있습니다. -
Web 에서
CoreDuration스케일 밖colorTransitionDuration/thumbTransitionDuration은 '동작 줄이기' 설정에 반응하지 않습니다. 스케일 위의 값은var(--coui-duration-*)로 나가 media query 가 덮지만 — 그 media query 는 전환 tier(0초과 ~CoreReducedMotion.transitionCeilingMs이하)의 변수만 0 으로 덮고 표시 tier 는 값을 유지합니다 — 그 밖의 값은 인라인{ms}ms리터럴로 나가 덮이지 않습니다. Flutter 는 두 duration 모두 억제합니다.CoreDuration의 alias(instant·fast…)나msN스케일 값을 쓰면 양쪽이 같아집니다.
크로스 플랫폼 차이점 (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> |
| 전환 렌즈 |
트랙 옆
Stacks
에
OutlinedContainer(role: functional)
— 진행도는
AnimationController
|
트랙 다음 flex 항목(마진이 크기를 상쇄) + @keyframes — 굴절은 문서 호스트가 붙임 |
수용된 호환 예외와 공통 호출#
Flutter는 value와 nullable onChanged 인자 전달을 요구하고 Web은 생략을 허용해 value를 false로 시작합니다. Web이 더 관대한 requiredness는 기존 호출 호환성을 위한 수용 예외입니다. 같은 controlled 호출을 공유할 때는 두 인자를 명시하세요. 이 예외는 런타임 컨트롤러·포커스 객체 타입 차이나 플랫폼별 상태 저장 정책과 별개입니다.
관련 컴포넌트 (Related Components)#
- Checkbox: 독립 선택과 indeterminate 상태. 적용·저장 시점은 두 컴포넌트 모두 호출자가 결정
- 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,
),
]),
)