SwitchField
라벨과 설명이 포함된 스위치 폼 필드 컴포넌트
목차
라벨과 보조 설명이 포함된 폼 친화적인 스위치 컴포넌트입니다.
기본 사용법 (Basic Usage)#
// 기본 스위치 필드
SwitchField(
value: isNotificationEnabled,
onChanged: handleNotificationToggled,
label: '알림 수신',
)
// 설명 포함
SwitchField(
value: isDarkMode,
onChanged: handleDarkModeToggled,
label: '다크 모드',
description: '어두운 테마를 사용합니다',
)
// 다른 크기 — 기본 토글은 lg(40×24)이고, 크기는 chrome 이므로
// switchFieldStyle 로 진입합니다
SwitchField(
value: isAutoSave,
onChanged: handleAutoSaveToggled,
label: '자동 저장',
description: '변경 사항을 자동으로 저장합니다',
switchFieldStyle: const CoreSwitchFieldStyle(size: CoreComponentSize.xl),
)
// 기본 스위치 필드
SwitchField(
value: isNotificationEnabled,
onChanged: handleNotificationToggled,
label: '알림 수신',
)
// 설명 텍스트 포함 — description으로 보조 설명 표시
SwitchField(
value: isDarkMode,
onChanged: handleDarkModeToggled,
label: '다크 모드',
description: '어두운 테마를 사용합니다',
)
// 비활성화 상태 — enabled: false로 상호작용 차단
SwitchField(
value: isAutoSave,
onChanged: handleAutoSaveToggled,
label: '자동 저장',
description: '변경 사항을 자동으로 저장합니다',
enabled: false,
)
Live Preview#
코드 보기
class SwitchFieldDefaultExample extends StatefulComponent {
const SwitchFieldDefaultExample({super.key});
@override
State<SwitchFieldDefaultExample> createState() =>
_SwitchFieldDefaultExampleState();
}
class _SwitchFieldDefaultExampleState
extends State<SwitchFieldDefaultExample> {
bool _value = false;
@override
Component build(BuildContext context) {
return SwitchField(
label: 'Enable notifications',
description: 'Receive push notifications.',
value: _value,
onChanged: (v) => setState(() => _value = v),
);
}
}
class SwitchFieldDefaultExample extends StatefulWidget {
const SwitchFieldDefaultExample({super.key});
@override
State<SwitchFieldDefaultExample> createState() =>
_SwitchFieldDefaultExampleState();
}
class _SwitchFieldDefaultExampleState
extends State<SwitchFieldDefaultExample> {
bool _value = false;
@override
Widget build(BuildContext context) {
return SwitchField(
label: 'Enable notifications',
description: 'Receive push notifications.',
value: _value,
onChanged: (v) => setState(() => _value = v),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 설정 화면에서 기능을 켜거나 끄는 이진 상태를 제어하는 경우
- 라벨과 설명 텍스트가 함께 필요한 스위치 형태의 폼 필드
- 알림, 다크 모드, 자동 저장 등 즉각 효과가 있는 토글 설정
- 폼 내에서 체크박스보다 직관적인 ON/OFF 표현이 필요한 경우
대신 다른 컴포넌트를 사용하세요:
Toggle: 라벨 없이 단독으로 사용하는 간단한 스위치Checkbox: 여러 항목의 선택 여부를 폼 내에서 관리하는 경우RadioGroup: 상호 배타적인 여러 옵션 중 하나를 선택하는 경우
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | bool | 명시 (Flutter 필수) | 현재 스위치 상태 |
onChanged |
ValueChanged<bool>? |
명시 (Flutter 필수, null 허용) | 상태 변경 콜백 |
label | String | 필수 | 스위치 라벨 |
labelMaxLines |
int? |
null |
라벨이 넘칠 때까지 허용하는 줄 수. null 은 현행대로 줄 수를 제한하지 않습니다 |
labelOverflow |
CoreTextOverflow? |
null |
labelMaxLines
에 도달한 라벨의 넘침 처리 (
clip
/
fade
/
ellipsis
/
visible
)
|
description |
String? |
null |
보조 설명 텍스트 |
enabled | bool | true | 활성화 여부 |
switchFieldStyle |
CoreSwitchFieldStyle? |
null |
인스턴스별 chrome 단일 진입점 |
스위치 필드가 합성하는 토글의 기본 크기는 lg(트랙 40×24, 손잡이 20)입니다. 꺼진 트랙은
surfaceContainerHighest 이고 Toggle 의 꺼진 트랙 기본값과 같은 단입니다 — 스위치 필드가 자기 트랙
색을 합성한 Toggle 에 덮어서 넘기므로 두 기본값은 함께 움직입니다. 설명은 labelSmall
(12 / 500 / 행간 140%)에 onSurfaceSubtle 색으로 그려지고, 비활성에서도 같은 설명 색입니다.
CoreSwitchFieldStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
size |
CoreComponentSize? |
Toggle size within the switch field. Forwarded to the composed
Toggle
as is, so
xs
is retiring here too (
CoreToggleContract.retiringSizes
) — the notice names
Toggle
.
|
labelToggleGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the horizontal gap between the label section and the toggle. Forwarded to the
Gap
widget rendered between the two siblings on Flutter, and drives the root
<div>
's inline
gap
(via
size
) on Web. Merged on top of [defaultLabelToggleGapStyle].
|
labelDescriptionGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the vertical gap between the label and the description inside the label section. Forwarded to the
Gap
widget rendered between the two
Text
siblings on Flutter, and drives the text column
<div>
's inline
row-gap
(via
size
) on Web. Merged on top of [defaultLabelDescriptionGapStyle].
|
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed
Clickable
(press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded.
|
minHeight |
double? |
Minimum height of the pressable row — label, description and toggle together — before scaling. The row's content is centred in the extra height, and the whole row stays the press area, so a theme can give every labelled switch a touch target (for example
CoreA11ySize.touchTarget
). Falls back to [defaultMinHeight].
|
activeColor | CoreColor? | Active (on) track colour. |
inactiveColor |
CoreColor? |
Inactive (off) track colour. |
labelTextStyle |
CoreTextStyle? |
Label text style when enabled (typography + colour). Merged with [defaultLabelTextStyle] by the resolver. |
disabledLabelTextStyle |
CoreTextStyle? |
Label text style when disabled. Merged with [defaultDisabledLabelTextStyle] by the resolver. |
descriptionTextStyle |
CoreTextStyle? |
Description text style when enabled. Merged with [defaultDescriptionTextStyle] by the resolver. |
disabledDescriptionTextStyle |
CoreTextStyle? |
Description text style when disabled. Merged with [defaultDisabledDescriptionTextStyle] by the resolver. |
빠른 오버라이드 (Chain)#
이미 만든 SwitchField 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class SwitchFieldChainExample extends StatefulWidget {
const SwitchFieldChainExample({super.key});
@override
State<SwitchFieldChainExample> createState() => _SwitchFieldChainExampleState();
}
class _SwitchFieldChainExampleState extends State<SwitchFieldChainExample> {
bool _value = false;
@override
Widget build(BuildContext context) {
return SwitchField(
label: 'Enable notifications',
description: 'Receive push notifications.',
value: _value,
onChanged: (v) => setState(() => _value = v),
).withStyle(
const CoreSwitchFieldStyle(
labelToggleGapStyle: CoreGapStyle(size: CoreSpace.space24),
labelDescriptionGapStyle: CoreGapStyle(size: CoreSpace.space4),
activeColor: CoreColor.token(CoreColors.success),
labelTextStyle: CoreTextStyle.token(
CoreTextStyles.titleSmall,
color: CoreColor.token(CoreColors.onSurface),
),
),
);
}
}
class SwitchFieldChainExample extends StatefulComponent {
const SwitchFieldChainExample({super.key});
@override
State<SwitchFieldChainExample> createState() => _SwitchFieldChainExampleState();
}
class _SwitchFieldChainExampleState extends State<SwitchFieldChainExample> {
bool _value = false;
@override
Component build(BuildContext context) {
return SwitchField(
label: 'Enable notifications',
description: 'Receive push notifications.',
value: _value,
onChanged: (v) => setState(() => _value = v),
).withStyle(
const CoreSwitchFieldStyle(
labelToggleGapStyle: CoreGapStyle(size: CoreSpace.space24),
labelDescriptionGapStyle: CoreGapStyle(size: CoreSpace.space4),
activeColor: CoreColor.token(CoreColors.success),
labelTextStyle: CoreTextStyle.token(
CoreTextStyles.titleSmall,
color: CoreColor.token(CoreColors.onSurface),
),
),
);
}
}
변형 (Variants)#
기본#
SwitchField(
value: emailMarketing,
onChanged: handleEmailMarketingToggled,
label: '마케팅 이메일 수신',
)
설명 포함#
SwitchField(
value: biometricLogin,
onChanged: handleBiometricToggled,
label: '생체 인증 로그인',
description: '지문 또는 얼굴 인식으로 로그인합니다',
)
동작 스펙 (Behavior)#
인터랙션#
- 클릭/탭: 스위치 토글 또는 레이블/설명 영역 클릭으로 상태 전환
- 호버: 포인터 오버 시 스위치에 시각적 강조 표시
- 포커스: 포커스 링으로 현재 포커스 상태 표시
상태 전환#
off→on(클릭 또는 Space/Enter 입력)on→off(다시 클릭 또는 Space/Enter 입력)disabled상태에서는 모든 인터랙션 무시
즉각 반영 vs 폼 제출#
- 설정 페이지에서 SwitchField는 일반적으로 클릭 즉시 효과 반영
- 폼 내에서 사용 시 다른 필드와 함께 제출 시 반영
사용 가이드라인 (Usage Guidelines)#
라벨·설명·스위치를 함께 클릭 가능한 설정 행으로 묶으려면 SwitchField를 사용합니다. 트랙만 합성하거나 prefix/suffix가 필요하면 Toggle, 콘텐츠 두 상태 교체는 Swap, 목록의 독립 선택·indeterminate 상태는 Checkbox를 선택합니다.
✅ Do#
description으로 스위치 효과를 명확히 설명
SwitchField(
value: locationTracking,
onChanged: handleLocationTrackingToggled,
label: '위치 정보 수집',
description: '앱 사용 중 위치 데이터를 수집하여 맞춤 서비스를 제공합니다',
)
스위치의 효과와 영향을 설명 텍스트로 명확히 전달하면 사용자가 안심하고 선택할 수 있다.
❌ Don't#
다중 선택이 필요한 경우에 SwitchField 사용
// ❌ 여러 항목 중 다수를 선택해야 하는 경우 Checkbox가 적합
Column(
children: [
SwitchField(value: receiveEmail, onChanged: handleEmailToggled, label: '이메일'),
SwitchField(value: receiveSms, onChanged: handleSmsToggled, label: 'SMS'),
SwitchField(value: receivePush, onChanged: handlePushToggled, label: '푸시 알림'),
// 이런 경우 CheckboxGroup이 더 적합
],
)
다수 항목의 선택/해제를 관리할 때는 Checkbox나 CheckboxGroup이 더 적합하다.
✅ Do#
설정 즉시 적용되는 스위치에 로딩 상태 처리
SwitchField(
value: isFeatureEnabled,
onChanged: (value) async {
unawaited(settingsService.updateFeature(value));
handleFeatureToggled(value);
},
label: '베타 기능 사용',
enabled: !isLoading, // API 호출 중 비활성화
)
비동기 설정 변경 중 중복 클릭을 방지하여 일관된 상태를 유지한다.
❌ Don't#
부정적 표현의 라벨 사용
// ❌ 부정 표현은 ON/OFF 상태와 혼란을 야기함
SwitchField(
value: isNotificationsDisabled, // 부정 상태를 저장
onChanged: handleNotificationsDisabledToggled,
label: '알림 끄기', // ON이 "알림 끄기"? 혼란스러움
)
스위치 라벨은 항상 긍정형으로 작성해야 ON 상태의 의미가 명확하다.
✅ Do#
레이블을 항상 Switch 옆에 배치하세요.
SwitchField(
label: '마케팅 알림 수신',
description: '이벤트 및 프로모션 알림을 받습니다.',
value: marketingEnabled,
onChanged: handleMarketingToggled,
)
레이블 없는 스위치는 무엇을 켜고 끄는지 알 수 없습니다. 레이블과 선택적 설명을 함께 제공하여 명확한 맥락을 만들어야 합니다.
❌ Don't#
Switch로 즉각 실행되는 위험한 동작을 제어하지 마세요.
// ❌ Switch 토글 즉시 계정 삭제 시작
SwitchField(
label: '계정 비활성화',
value: isDeactivated,
onChanged: (value) {
if (value) deactivateAccountImmediately(); // 위험한 즉시 실행
},
)
Switch는 즉각적인 ON/OFF 상태 변경에 적합합니다. 계정 삭제, 데이터 초기화 등 위험한 동작은 Dialog로 확인을 받으세요.
접근성 (Accessibility)#
역할#
SwitchField 는 자기 시맨틱을 직접 선언하지 않습니다 — 역할은 합성한 두 컴포넌트에서
따로 옵니다.
-
Web: 행 전체를 감싼
Clickable이 루트<div role="button">을 그리고 (활성이면tabindex="0", 비활성이면aria-disabled="true"이면서tabindex없음),role="switch"+aria-checked는 안쪽Toggle의<div>에 붙습니다 (활성tabindex="0"/ 비활성tabindex="-1"+aria-disabled). -
Flutter:
switch_field.dart에는Semantics가 하나도 없습니다. 바깥Clickable이Semantics(button: true, enabled: <enabled>)를, 안쪽Toggle이Semantics(toggled: <value>)를 붙입니다.
즉 양 플랫폼 모두 버튼 역할과 스위치 상태가 서로 다른 노드에 나뉘어 있습니다.
라벨은 Web 에서 <label> 요소로 렌더되지만 for 도 id
도 없어 어떤 컨트롤과도
연결되지 않고, aria-label / aria-labelledby / aria-describedby
는 어느
요소에도 붙지 않습니다.
키보드#
| 키 | 동작 |
|---|---|
Space | 스위치 상태 토글 |
Enter | 스위치 상태 토글 |
Tab | 다음 포커스 가능 요소로 이동 |
Shift+Tab | 이전 포커스 가능 요소로 이동 |
활성화 키는 양 플랫폼 모두 Clickable 이 처리합니다 — Flutter 는
SingleActivator(.enter) / SingleActivator(.space)
를 ActivateIntent 로,
Web 은 keydown 에서 Enter · Space 를 받습니다. Web 쪽 핸들러는
event.target 이 자기 자신일 때만 반응하므로, 안쪽 스위치에서 누른 키가 바깥 행
핸들러까지 올라가 두 번 토글되는 일은 없습니다. enabled: false 면 어느 키도
동작하지 않습니다.
스크린 리더#
-
이름:
label과description텍스트는 바깥 노드(Webrole="button"/ Flutterbutton: true)의 내용으로만 읽힙니다.role="switch"(Web) /toggled노드(Flutter)에는 자기 이름이 없습니다 — 안쪽Toggle에label/prefix/suffix를 넘기지 않기 때문입니다. -
상태: on/off 는 Web
aria-checked, FlutterSemantics(toggled:)로 전달됩니다. 비활성은 Webaria-disabled, FlutterSemantics(enabled: false). - 통보: 라이브 리전이 아닙니다.
포커스 관리#
오버레이가 아니므로 진입 시 포커스 이동이 없고 트랩도 없습니다.
한 SwitchField 는 tab stop 을 두 개 만듭니다 — 행 전체(Clickable)와 안쪽
스위치(Toggle 의 Clickable)가 각각 포커스를 받습니다. 안쪽 Toggle
은 포인터만
차단되고(Flutter IgnorePointer, Web pointer-events-none) 포커스 traversal 에서는
빠지지 않습니다. 두 정지점 모두 Space / Enter 로 토글되며, 안쪽 Toggle
의
포커스를 끌 파라미터는 노출되어 있지 않습니다. enabled: false 면 두 정지점 모두
사라집니다.
포커스 링은 양 플랫폼 모두 Clickable 이 그립니다 — Flutter 는 FocusOutline,
Web 은 focus-visible: 링.
터치 타깃 (눌림 영역 최소 높이)#
라벨·설명을 포함한 행 전체가 하나의 눌림 영역입니다. 기본값은 바닥이 없어(minHeight = 0) 행이 내용 높이 그대로이며, 누르는 영역의 하한은 공통 24 를 따릅니다. 터치 화면에서 48 이 필요한 앱은 테마로 바닥을 켭니다 — 행이
minHeight × scaling 이상이 되고 내용은 가운데에 놓이며, 늘어난 높이까지 눌립니다.
ThemeData(
coreComponentTheme: CoreComponentTheme(
switchField: CoreSwitchFieldTheme(
style: CoreSwitchFieldStyle(minHeight: CoreA11ySize.touchTarget),
),
),
)
Web 은 같은 값을 행에 min-height 로 냅니다.
알려진 제약#
-
SwitchField에는 caller passthrough 진입점이 없습니다. 조건: 양 플랫폼 모든 인스턴스. Web 생성자는super.key와super.id만 선언해attributes·classes·css· 이벤트 파라미터를 받지 않고,copyWith는attributes/classes/css를 인자로 받지만 생성자에 넘기지 않고 버립니다. Flutter 에도semanticLabel류 슬롯이 없습니다. 따라서aria-label·aria-labelledby·aria-describedby나 더 구체적인 역할을 밖에서 실을 수 없습니다. 대안 — Flutter 는 호출부에서Semantics로 감싸는 우회가 있지만 Web 은 없습니다. -
description이 설명으로 노출되지 않습니다. 조건:description != null인 모든 경우.aria-describedby(Web) 도Semantics(hint:)(Flutter) 도 배선되어 있지 않아, 설명이 별도 설명이 아니라 바깥 버튼 이름의 일부로 함께 읽힙니다. 대안: 없음 (위와 같은 이유로 밖에서 연결할 진입점이 없습니다). -
onChanged없이enabled: true로 두면 아무 동작 없는 tab stop 이 남습니다. 조건:enabled기본값(true) +onChanged: null. 양 플랫폼 모두Clickable의enabled가onChanged유무와 무관하게enabled만 보므로 포커스·aria-disabled상태가 활성으로 남습니다. 대안: 표시 전용이면enabled: false를 명시합니다.
크로스 플랫폼 차이점 (Platform Differences)#
named properties 표면(label · description · value · onChanged
· enabled · switchFieldStyle)은 양 플랫폼이 동일합니다. 아래는 렌더 경로 차이입니다.
| 항목 | Flutter | Web |
|---|---|---|
| 렌더링 | Clickable + Row + Toggle |
Clickable + <div> + Toggle |
| chrome 적용 | resolver 가 굽는 Flutter 값 | resolver 가 내보내는 class / inline CSS |
수용된 호환 예외와 공통 호출#
Flutter는 value와 nullable onChanged 인자를 요구하며 Web은 둘 다 생략할 수 있습니다. Web이 더 관대한 requiredness는 기존 호출 호환성을 위한 수용 예외입니다. 두 플랫폼의 공통 제어 호출에서는 두 인자를 명시하세요. 이 예외와 위 렌더 엔진 차이는 구분합니다.
관련 컴포넌트 (Related Components)#
- Toggle: 라벨 없이 단독으로 사용하는 최소한의 스위치
- Checkbox: 폼에서 항목 선택 여부를 관리하는 경우
- RadioGroup: 여러 옵션 중 하나를 배타적으로 선택하는 경우
조합 예제#
// SwitchField 목록 + Fieldset 조합: 알림 설정 섹션
Fieldset(
legend: Text('알림 설정').titleSmall.onSurface,
children: [
SwitchField(
value: pushEnabled,
onChanged: handlePushToggled,
label: '푸시 알림',
description: '새 메시지와 업데이트를 알려드립니다',
),
SwitchField(
value: emailEnabled,
onChanged: handleEmailToggled,
label: '이메일 알림',
description: '주간 요약 및 중요 공지를 이메일로 받습니다',
),
SwitchField(
value: marketingEnabled,
onChanged: handleMarketingToggled,
label: '마케팅 알림',
description: '할인 혜택 및 이벤트 정보를 받습니다',
),
],
)