Checkbox | CoUI
본문으로 건너뛰기
LogoCoUI

Checkbox

하나 이상 선택하는 체크박스. 불확정(indeterminate) 상태 지원

목차

하나 이상의 항목을 선택할 수 있는 체크박스 컴포넌트입니다. 불확정(indeterminate) 상태를 지원합니다.

기본 사용법 (Basic Usage)#

값은 Flutter 의 tristate bool? 입니다 — true / false / null(불확정). onChanged 는 다음 값을 받습니다.

// 기본 체크박스
Checkbox(
  checked: false,
  onChanged: handleChanged,
  label: '이용약관에 동의합니다',
)

// 체크된 상태
Checkbox(
  checked: true,
  onChanged: handleChanged,
  label: '알림 받기',
)

// 불확정 상태
Checkbox(
  checked: null,
  onChanged: handleChanged,
  tristate: true,
  label: '전체 선택',
)
// 기본 체크박스
Checkbox(
  checked: false,
  onChanged: handleChanged,
  label: '이용약관에 동의합니다',
)

// 체크된 상태
Checkbox(
  checked: true,
  onChanged: handleChanged,
  label: '알림 받기',
)

// 불확정 상태
Checkbox(
  checked: null,
  onChanged: handleChanged,
  label: '전체 선택',
)

Live Preview#

구조 (Anatomy)#

코드가 정본입니다. 부분 이름은 위젯 슬롯과 같고, Figma ❖ Checkbox 레이어와도 맞습니다.

부분역할슬롯
prefix인디케이터 왼쪽prefix
suffix라벨 오른쪽suffix
trailing행 끝trailing

사용 시기 (When to Use)#

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

  • 여러 옵션 중 하나 이상을 선택할 때 (다중 선택)
  • 약관 동의, 옵션 활성화 등 boolean 값을 입력받을 때
  • "전체 선택" 같은 부분 선택 상태(indeterminate)를 표현할 때

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

  • SwitchField: 체크·불확정 상태 없이 라벨·설명을 가진 bool 설정 행이 필요할 때
  • Toggle: 체크·불확정 표시 없이 on/off 스위치 표현이 필요할 때
  • RadioGroup: 여러 옵션 중 하나만 선택해야 할 때
  • Select: 옵션 목록을 펼쳐 두는 대신 드롭다운에서 값 하나를 선택할 때

Props / Parameters#

W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. W? 는 선택 슬롯입니다.

속성타입기본값설명
checked bool? 명시¹ 체크 값 — true / false / null(불확정)
onChanged void Function(bool?)? 명시¹ 다음 값을 받는 콜백. 표시 전용이면 null
labelString?null라벨 텍스트
labelMaxLines int? null 라벨이 넘칠 때까지 허용하는 줄 수. null 은 현행대로 줄 수를 제한하지 않습니다
labelOverflow CoreTextOverflow? null labelMaxLines 에 도달한 라벨의 넘침 처리 ( clip / fade / ellipsis / visible )
description String? null 라벨 아래 보조 텍스트
tristate bool false 탭이 true → false → null → true 로 순환
enabled bool? null 활성화 여부 (null = onChanged != null)
size CoreComponentSize md 크기 토큰 (xs / sm / md / lg / xl / xl2)
prefixW?null인디케이터 왼쪽 슬롯
suffixW?null라벨을 대신하는 슬롯
trailingW?null행 끝 슬롯
nameString?null폼 필드 이름
checkboxStyle CoreCheckboxStyle? null 박스 chrome / 상태 레이어 / 중첩 슬롯 묶음 (아래 Style 표 참고)
focusNode FocusNode? null Flutter 전용 — Web은 브라우저 기본 포커스
statesController WidgetStatesController? null Flutter 전용 — Web은 CSS pseudo-class
controller CheckboxController? null Flutter 전용 — 외부에서 상태를 제어할 때

¹ 공통 호출에서는 checked 와 onChanged 를 모두 명시합니다. Flutter는 두 인자를 요구하고 Web은 생략 시 false / null 을 쓰는 기존 호환 완화를 유지합니다. 이는 새 동작 옵션이 아니라 Web의 soft requiredness 예외입니다.

값은 tristate bool? 하나입니다 — true / false / null(불확정). 크기 외의 시각 축은 없어 variant 인자도 없습니다. hover / focus / pressed 는 prop 이 아닙니다 — 각 플랫폼이 감지하고 chrome 은 checkboxStyle 의 상태별 필드(stateLayer*, clickableStyle.focusOutlineStyle)에서 읽습니다.

빠른 오버라이드 (Chain)#

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

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

  @override
  State<CheckboxChainExample> createState() => _CheckboxChainExampleState();
}

class _CheckboxChainExampleState extends State<CheckboxChainExample> {
  bool? _checked = false;

  @override
  Widget build(BuildContext context) {
    return Checkbox(
      checked: _checked,
      label: 'Accept terms',
      onChanged: (value) => setState(() => _checked = value),
    ).withStyle(
      const CoreCheckboxStyle(
        boxBorderColor: CoreColor.token(CoreColors.primary),
        boxBorderWidth: CoreStrokeWidth.stroke2,
        boxBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
        boxSize: CoreSize.size24,
        indicatorLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
      ),
    );
  }
}
class CheckboxChainExample extends StatefulComponent {
  const CheckboxChainExample({super.key});

  @override
  State<CheckboxChainExample> createState() => _CheckboxChainExampleState();
}

class _CheckboxChainExampleState extends State<CheckboxChainExample> {
  bool? _checked = false;

  @override
  Component build(BuildContext context) {
    return Checkbox(
      checked: _checked,
      label: 'Accept terms',
      onChanged: (value) => setState(() => _checked = value),
    ).withStyle(
      const CoreCheckboxStyle(
        boxBorderColor: CoreColor.token(CoreColors.primary),
        boxBorderWidth: CoreStrokeWidth.stroke2,
        boxBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
        boxSize: CoreSize.size24,
        indicatorLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
      ),
    );
  }
}

스타일 시스템 (Style System)#

Checkbox 의 box chrome / 상태 레이어 / 슬롯 미세 조정은 단일 checkboxStyle (CoreCheckboxStyle) 으로 흐릅니다. 시맨틱 enum (size) 은 위젯 파라미터. 보더(stroke1_5, 활성 경계)와 모서리(radius4)는 사이즈와 무관한 flat 기본값이고, 박스·글리프 길이·gap·라벨·설명 fontSize(설명은 11 / 12 / 13 / 14 / 14 / 14 로 라벨 한 단 아래) 만 defaultsBySize 사다리를 탑니다.

체크 표시는 평범한 LucideIcons.check 아이콘이라 별도 예외 없이 Lucide 획 규칙을 따르고, 체크박스 전 크기에서 하한 1.25로 그려집니다. markThickness 는 일부 선택 가로줄의 두께만 정하며 기본값이 그 규칙의 하한과 같아서, 모든 크기에서 체크와 가로줄이 같은 두께로 읽힙니다.

Checkbox(
  size: CoreComponentSize.md,
  checked: true,
  onChanged: handleChange,
  label: '동의함',
  checkboxStyle: CoreCheckboxStyle(
    boxBorderColor: CoreColor.token(CoreColors.outline),
    boxActiveColor: CoreColor.token(CoreColors.primary),
    boxBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
    // hover 8% / pressed 16% halo — 박스 +8, radius 8
    stateLayer: CoreStateLayer.standard,
    stateLayerSpread: CoreSpace.space4,
    indicatorLabelGapStyle: CoreGapStyle(size: CoreSpace.space8),
    checkmarkIconStyle: CoreIconStyle(
      size: CoreSize.size16,
      color: CoreColor.token(CoreColors.onPrimary),
    ),
    labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
  ),
)

CoreCheckboxStyle 필드#

필드타입설명
boxBackgroundColor CoreColor? Resting (unchecked) box fill colour override. null defers to [defaultBoxBackgroundColor].
boxActiveColor CoreColor? Box fill colour when checked or indeterminate — the border takes the same colour, so the stroke blends into the fill. null defers to [defaultBoxActiveColor].
boxBorderColor CoreColor? Resting (unchecked) box stroke colour override. null defers to [defaultBoxBorderColor].
boxBorderWidth double? Box stroke width override (logical px, pre-scaling). null defers to [defaultBoxBorderWidth].
boxBorderRadius CoreBorderRadius? Box corner override. null defers to [defaultBoxBorderRadius]. The focus ring's corner derives from this one (plus its own offset), so the ring stays concentric with a rounder box.
boxSize double? Box size (width = height) override (logical px). null defers to [defaultsBySize] for the widget's size.
checkmarkSize double? Checkmark glyph icon size override (logical px). null falls back to [defaultsBySize] for the widget's size + any nested checkmarkIconStyle.size override.
indeterminateLength double? Indeterminate dash length override (logical px) — the dash is a horizontal pill [markThickness] tall. null falls back to [defaultsBySize] for the widget's size.
indeterminateBorderRadius CoreBorderRadius? Corner of the indeterminate dash. null → [defaultIndeterminateBorderRadius] (a pill).
markThickness double? Thickness of the indeterminate dash — one constant across sizes. The checkmark is an icon and follows the icon stroke rule, not this value. null defers to [defaultMarkThickness].
disabledBackgroundColor CoreColor? Box fill when disabled. null → [defaultDisabledBackgroundColor].
disabledBorderColor CoreColor? Box stroke when disabled. null → [defaultDisabledBorderColor].
disabledForegroundColor CoreColor? Label and description foreground when disabled. null → [defaultDisabledForegroundColor].
disabledCheckmarkColor CoreColor? Checkmark / dash colour when disabled. null → [defaultDisabledCheckmarkColor].
stateLayer CoreStateLayer? Ramp the halo is painted at while hovered or pressed. null → [defaultStateLayer]. The focus rung is unused — focus draws the ring.
stateLayerColor CoreColor? Halo ink. null → [defaultStateLayerColor].
stateLayerSpread double? How far the halo reaches beyond the box on each side (logical px, pre-scaling). null → [defaultStateLayerSpread].
stateLayerBorderRadius CoreBorderRadius? Halo corner. null → [defaultStateLayerBorderRadius].
indicatorLabelGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between the checkbox indicator (box) and the label / prefix / suffix slot — forwarded straight to the Gap widget that separates the slots. null defers to [defaultsBySize] for the widget's size, then to [defaultIndicatorLabelGapStyle].
transitionDuration Duration? State-change transition duration override (box colour fade + glyph slot scale / opacity + halo fade). null defers to [defaultTransitionDuration].
checkmarkIconStyle CoreIconStyle? Checkmark icon style override (size / colour). Merged on top of CoreIconStyle(size: checkmarkSize) and [defaultCheckmarkIconStyle], so a colour-only override keeps the size-ladder glyph and a size-only override keeps the onPrimary ink.
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed Clickable (press scale / durations / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded; the Clickable's own resolver fills the remaining defaults. Its focusOutlineStyle half is inert here — see [focusOutlineStyle].
focusOutlineStyle CoreFocusOutlineStyle? Nested [CoreFocusOutlineStyle] slot for the ring the checkbox draws around its box while focused. Merged on top of [defaultFocusOutlineStyle]; the corner comes from the box.
labelStyle CoreTextStyle? Label text style override.
descriptionStyle CoreTextStyle? Description text style override — the supporting line under the label. null defers to [defaultsBySize] for the widget's size (which carries only the fontSize step), then to [defaultDescriptionStyle] for the role and colour.
labelDescriptionGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the vertical gap between the label and the description below it. null defers to [defaultLabelDescriptionGapStyle].
minHeight double? Minimum height of the pressable row — box, label and description together — before scaling. The row keeps its cross-axis alignment in the extra height and stays the press area as a whole, so a theme can give every checkbox a touch target (for example CoreA11ySize.touchTarget ). Falls back to [defaultMinHeight].

Resolve chain#

design system default (flat default* + defaultsBySize[size])
  → CoreCheckboxTheme.style
  → 부모 컴포넌트 슬롯 오버라이드
  → widget.checkboxStyle

변형 (Variants)#

값#

// 체크됨
Checkbox(
  checked: true,
  label: '선택됨',
  onChanged: handleChanged,
)

// 불확정
Checkbox(
  checked: null,
  label: '일부 선택',
  onChanged: handleChanged,
)

// 비활성화 — 박스·마크·라벨 모두 disabled 토큰 계열
Checkbox(
  checked: false,
  enabled: false,
  label: '비활성',
  onChanged: handleChanged,
)

상태 (hover / focus / pressed)#

prop 이 아니라 감지입니다. hover 8% · pressed 16% 의 on-surface halo 가 박스 바깥 +8 (radius 8) 에 깔리고, 키보드 포커스는 박스를 감싸는 링(+4, focus-ring) 으로 표시됩니다 — 행 전체가 아닙니다. 박스와 링 사이 2px 간격은 투명이라 체크박스가 놓인 배경이 보입니다.

동작 스펙 (Behavior)#

상태 전환#

  • 2단계 (tristate: false): true ↔ false 토글
  • 3단계 (tristate: true): true → false → null(불확정) → true 순환
  • checked 는 호출부의 값입니다. 활성화는 다음 값으로 onChanged 를 호출할 뿐 자체 선택 값을 바꾸지 않으므로 부모가 갱신해야 합니다. enabled: false 는 활성화·포커스 진입을 막고, enabled 를 생략하면 콜백 유무로 결정합니다.

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

✅ Do#

체크박스에 항상 라벨을 제공하세요.

Checkbox(
  checked: false,
  onChanged: handleChanged,
  label: '마케팅 이메일 수신에 동의합니다',
)

라벨 영역도 클릭 가능하여 조작이 쉬워집니다.


❌ Don't#

라벨 없이 체크박스만 두지 마세요.

Checkbox(
  checked: false,
  onChanged: handleChanged,
)

체크박스의 목적을 알 수 없고, 작은 영역만 클릭 가능합니다.

✅ Do#

긍정형 라벨을 사용하세요.

Checkbox(label: '알림 받기', checked: false, onChanged: handleChanged)

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


❌ Don't#

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

Checkbox(label: '알림 받지 않기', checked: false, onChanged: handleChanged)

체크하면 "받지 않기"가 되어 이중 부정으로 혼란스럽습니다.

접근성 (Accessibility)#

역할#

  • Flutter — Semantics(checked: ..., mixed: ...) 로 체크 상태를 내보냅니다. 합성된 Clickable 에 semanticsButton: false 를 넘겨 일반 "버튼" 역할은 꺼져 있고, Clickable 이 붙이는 Semantics(enabled:) 는 그대로 남습니다.
  • Web — 인터랙션 루트에 role="checkbox" · aria-checked="true" | "false" | "mixed" · tabindex 가 붙고, 비활성이면 aria-disabled="true" 가 더해집니다.

name 을 주면 폼 제출용 히든 <input type="checkbox"> 가 함께 나가지만 sr-only · tabindex="-1" · aria-hidden="true" 라 접근성 트리에는 들어가지 않습니다 — 상태는 위의 role="checkbox" 노드가 나릅니다.

키보드#

키FlutterWeb
Space토글토글
Enter토글토글
Tab다음 포커스 가능 요소로다음 포커스 가능 요소로
Shift+Tab이전 포커스 가능 요소로이전 포커스 가능 요소로

활성화 키는 양쪽 다 합성된 Clickable 이 처리합니다 — 네이티브 체크박스와 달리 Space 뿐 아니라 Enter 로도 토글됩니다. tristate: true 면 키 활성화도 포인터와 같은 3-상태 순환(체크 → 해제 → 부분 선택)을 탑니다.

비활성 체크박스는 양 플랫폼 모두 Tab 순회에서 빠집니다 — Flutter 는 FocusableActionDetector(enabled: false), Web 은 tabindex="-1".

스크린 리더#

접근 가능한 이름은 role="checkbox" 노드의 내용에서 옵니다 — label(또는 suffix · prefix)에 넣은 텍스트가 그대로 이름이 됩니다. 셋 다 비우면 이름이 비고, 리더는 역할과 상태만 읽습니다(아래 알려진 제약 참고).

부분 선택(CoreCheckboxState.indeterminate)은 양쪽 다 별도로 통보됩니다 — Flutter 는 Semantics(mixed: true), Web 은 aria-checked="mixed". 해제로 반올림되지 않습니다.

체크가 바뀐 사실을 따로 알리는 live region 은 없습니다 — 상태는 체크박스 노드 자체에 실려 있습니다.

포커스 관리#

  • 진입 — 이동 없음. 오버레이가 아니라 인라인 컨트롤이라 마운트 시 포커스를 옮기지 않습니다.
  • 이탈 — 해당 없음.
  • 트랩 — 없음. Tab 은 체크박스를 지나 그대로 빠져나갑니다. 단 Web 은 한 체크박스에서 두 번 멈춥니다(아래 알려진 제약).
  • 포커스 표시 — 합성된 Clickable 의 포커스 링이 양쪽에서 키보드 포커스일 때만 나타납니다(focusOutline 기본값 true).

터치 타깃 (눌림 영역 최소 높이)#

라벨·설명을 포함한 행 전체가 하나의 눌림 영역입니다. 기본값은 바닥이 없어(minHeight = 0) 행이 내용 높이 그대로이며, 누르는 영역의 하한은 공통 24 를 따릅니다. 터치 화면에서 48 이 필요한 앱은 테마로 바닥을 켭니다 — 행이 minHeight × scaling 이상이 되고 내용은 가운데에 놓이며, 늘어난 높이까지 눌립니다.

ThemeData(
  coreComponentTheme: CoreComponentTheme(
    checkbox: CoreCheckboxTheme(
      style: CoreCheckboxStyle(minHeight: CoreA11ySize.touchTarget),
    ),
  ),
)

Web 은 같은 값을 행에 min-height 로 냅니다.

알려진 제약#

  • Web 은 role="checkbox" 를 중첩된 두 요소에 동시에 붙입니다. 컴포넌트가 계산한 속성 묶음(role · aria-checked · tabindex)이 바깥 인터랙션 루트와 안쪽 사각형 <div> 양쪽에 그대로 실려서, 스크린 리더는 체크박스 안에 체크박스가 하나 더 있는 것으로 읽고 Tab 은 하나의 체크박스에서 두 번 멈춥니다. 그중 안쪽 사각형에 포커스가 있을 때는 Space · Enter 가 아무 일도 하지 않습니다 — 키보드 활성화는 인터랙션 루트 자신이 포커스일 때만 동작하도록 이벤트 대상이 게이트되어 있고, 포인터 클릭만 루트까지 버블링됩니다. id 를 넘겼다면 같은 이유로 DOM 에 두 번 나타납니다. 두 요소 모두 컴포넌트 내부에서 만들어져 호출부가 끊을 수 있는 지점이 없습니다.
  • 라벨 없이 쓰면 이름이 비고, Web 에는 이름을 채울 다른 경로가 없습니다. 이름은 label 이나 suffix 의 내용에서만 오는데, Web Checkbox 는 attributes · classes · css 를 생성자에서 받지 않습니다 — copyWith 가 인자로 받아들이기는 하지만 새 인스턴스로 넘기지 않습니다. 표의 전체 선택 열처럼 시각 라벨을 둘 수 없는 자리에서는 Flutter 만 호출부의 Semantics(label: ...) 로 이름을 줄 수 있습니다.
  • Web 에서 CoreDuration 스케일 밖 transitionDuration 은 '동작 줄이기' 설정에 반응하지 않습니다. 스케일 위의 값은 var(--coui-duration-*) 로 나가 media query 가 덮지만 — 그 media query 는 전환 tier(0 초과 ~ CoreReducedMotion.transitionCeilingMs 이하)의 변수만 0 으로 덮고 표시 tier 는 값을 유지합니다 — 그 밖의 값은 인라인 {ms}ms 리터럴로 나가 덮이지 않습니다. Flutter 는 어떤 값이든 억제합니다. CoreDuration 의 alias(instant · fast …)나 msN 스케일 값을 쓰면 양쪽이 같아집니다.

스크린 리더 / ARIA#

체크박스 하나는 접근성 트리에서 노드 하나, 탭 순서에서 정지 하나입니다.

  • Web — role="checkbox" · aria-checked(true / false / 부분 선택이면 mixed) · 비활성일 때 aria-disabled="true" · tabindex, 그리고 호출자가 준 id 는 인터랙션 루트 한 곳에만 실립니다. 안쪽의 네모 상자는 순수한 시각 요소라 아무 정체성도 갖지 않습니다 — 같은 id 가 문서에 둘 생기거나 체크박스 안에 체크박스가 읽히지 않습니다.
  • Flutter — Semantics(checked:, mixed:) 가 상태를 말하고, 일반 버튼 역할은 걷어내 "버튼" 이 아니라 "체크박스" 로 읽힙니다. 비활성이면 비활성으로 발표됩니다.
  • 접근 가능한 이름은 양 플랫폼 모두 루트의 내용 — label(과 description) 텍스트 — 에서 옵니다. 라벨 없이 쓰면 이름이 없으므로, 그때는 prefix / suffix 로 텍스트를 넣거나 호출자가 이름을 붙여야 합니다.
  • 폼 제출용 숨은 입력 — Web 은 name 을 줄 때만 <input type="checkbox"> 를 함께 그립니다. 이 입력은 aria-hidden="true" · tabindex="-1" 이라 리더에도 탭 순서에도 나타나지 않고, 폼 값만 나릅니다.

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

항목FlutterWeb
체크 의미 Semantics(checked:, mixed:) Clickable 루트의 checkbox 역할·aria-checked
폼 값 FormValueSupplier 및 Flutter 컨트롤러 FormValueSupplier 와 이름이 있을 때 숨은 native input
포커스·상태 인프라 FocusNode, WidgetStatesController DOM 포커스 · pseudo-class
최소 히트 영역TouchTarget 래퍼합성된 Clickable의 touch-target 마커
틀 굵기 1.5 1.5 그대로 1배 화면은 브라우저가 1.5 CSS px 를 기기 픽셀로 내려 1px 로 그릴 수 있음 (2배 이상은 3 기기 픽셀)
  • Toggle: on/off 스위치. 즉시 적용되는 설정에 적합
  • Select: 드롭다운 선택. 옵션이 5개 이상이면 Select가 더 적합
  • Form: 폼 컨테이너. 체크박스를 폼 필드로 등록하여 일괄 검증 가능