MultiAnswerCard | CoUI
LogoCoUI

MultiAnswerCard

선택 상태를 표시하는 카드형 답안 선택 컴포넌트

MultiAnswerCard#

퀴즈·설문처럼 답안을 카드 형태로 보여주고 선택 상태를 강조하는 컴포넌트입니다. 불투명한 [value]를 가지며, selected 상태에 따라 테두리·배경 색이 달라집니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 답안 항목을 카드로 보여주고 선택 여부를 강조해야 하는 경우
  • 다중 선택 설문/퀴즈 UI

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

  • Checkbox: 텍스트 라벨 옆 체크박스로 충분한 경우
  • MultipleChoice: 그룹 단위 선택 관리가 필요한 경우

기본 사용법 (Basic Usage)#

MultiAnswerCard<String>(
  value: 'a',
  selected: picked.contains('a'),
  onChanged: (v) => setState(() => toggle(v)),
  child: const Text('Answer A'),
)
MultiAnswerCard<String>(
  value: 'a',
  selected: picked.contains('a'),
  onChanged: (v) => setState(() => toggle(v)),
  child: const Text('Answer A'),
)

빠른 오버라이드 (Chain)#

이미 만든 MultiAnswerCard 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 == CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

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

  @override
  State<MultiAnswerCardChainExample> createState() => _MultiAnswerCardChainExampleState();
}

class _MultiAnswerCardChainExampleState extends State<MultiAnswerCardChainExample> {
  final Set<String> _selected = {'a'};

  void _toggle(String value) {
    setState(() {
      if (_selected.contains(value)) {
        _selected.remove(value);
      } else {
        _selected.add(value);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        MultiAnswerCard<String>(
          value: 'a',
          selected: _selected.contains('a'),
          onChanged: _toggle,
          child: const Text('Answer A'),
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        MultiAnswerCard<String>(
          value: 'b',
          selected: _selected.contains('b'),
          onChanged: _toggle,
          child: const Text('Answer B'),
        ).withStyle(
          const CoreMultiAnswerCardStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
    );
  }
}
class MultiAnswerCardChainExample extends StatefulComponent {
  const MultiAnswerCardChainExample({super.key});

  @override
  State<MultiAnswerCardChainExample> createState() => _MultiAnswerCardChainExampleState();
}

class _MultiAnswerCardChainExampleState extends State<MultiAnswerCardChainExample> {
  final Set<String> _selected = {'a'};

  void _toggle(String value) {
    setState(() {
      if (_selected.contains(value)) {
        _selected.remove(value);
      } else {
        _selected.add(value);
      }
    });
  }

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        MultiAnswerCard<String>(
          value: 'a',
          selected: _selected.contains('a'),
          onChanged: _toggle,
          child: Text('Answer A'),
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        MultiAnswerCard<String>(
          value: 'b',
          selected: _selected.contains('b'),
          onChanged: _toggle,
          child: Text('Answer B'),
        ).withStyle(
          const CoreMultiAnswerCardStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

Props / Parameters#

속성타입기본값설명
valueT카드가 나타내는 값 (필수)
selectedboolfalse선택 상태
enabledbooltrue상호작용 가능 여부
onChanged void Function(T)? null 카드 탭 시 콜백 (value 전달)
childWidget / Component카드 내용 (필수)
multiAnswerCardStyle CoreMultiAnswerCardStyle? null chrome 슬롯 (아래 표)

CoreMultiAnswerCardStyle 필드#

필드타입설명
backgroundColor CoreColor? Card background colour override (unselected).
selectedBackgroundColor CoreColor? Card background colour override (selected).
borderColor CoreColor? Card border colour override (unselected).
selectedBorderColor CoreColor? Card border colour override (selected).
hoverColor CoreColor? Card hover background colour override.
borderWidth double? Card border width override (unselected, logical px).
selectedBorderWidth double? Card border width override (selected, logical px).
borderRadius CoreBorderRadius? Card border radius override.
padding CoreEdgeInsets? Card content padding override.
transitionDuration Duration? Transition duration override for selected / hover state changes; null → [defaultTransitionDuration].
bodyTextStyle CoreTextStyle? Body text style for the caller-supplied child widget (typography role + colour in one CoreTextStyle ). null defers to [defaultBodyTextStyle] — the design-system base typography, so the card text matches the Web side's inherited bodyMedium base.
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed Clickable (press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.

동작 스펙 (Behavior)#

인터랙션#

  • 클릭/탭: onChangedvalue 전달
  • 선택 상태: 두꺼운 테두리(primary) + 선택 배경
  • 호버: 미선택 카드는 hover 배경색 적용
  • enabledfalse이면 비활성 (커서 forbidden)

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

✅ Do#

selected 는 호출자가 계산해서 전달

MultiAnswerCard<String>(
  value: option.id,
  selected: picked.contains(option.id),
  onChanged: (v) => setState(() => toggle(v)),
  child: Text(option.label),
)

value 는 불투명한 식별자일 뿐입니다 — 카드 자신은 다른 카드의 선택 상태를 모르므로, selected 는 항상 호출자가 공유 상태(picked)에서 계산해 넘겨야 합니다.


❌ Don't#

읽기 전용 카드에 enabled: false를 명시하지 않고 넘어가지 않기

// ❌ onChanged 만 생략 — enabled 기본값(true)이라 hover/press 피드백이 그대로 나타남
MultiAnswerCard<String>(
  value: 'a',
  selected: true,
  child: const Text('Answer A'),
)

enabled 기본값은 true 이므로 onChanged 를 생략해도 hover 배경과 Clickable 의 press/focus 피드백은 계속 나타나 상호작용 가능한 것처럼 보입니다. 읽기 전용 카드에는 enabled: false 를 명시하세요.

접근성 (Accessibility)#

  • role="button" + aria-pressed (선택 상태)
  • 비활성 시 aria-disabled="true"

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

항목FlutterWeb
클래스명MultiAnswerCardMultiAnswerCard
전환 애니메이션AnimatedContainerCSS transition-all
호버 MouseRegion mouseenter / mouseleave