MultiAnswerCard#
퀴즈·설문처럼 답안을 카드 형태로 보여주고 선택 상태를 강조하는 컴포넌트입니다. 불투명한 [value]를 가지며, selected 상태에 따라 테두리·배경 색이 달라집니다.
Live Preview#
class MultiAnswerCardDefaultExample extends StatefulComponent {
const MultiAnswerCardDefaultExample({super.key});
@override
State<MultiAnswerCardDefaultExample> createState() =>
_MultiAnswerCardDefaultExampleState();
}
class _MultiAnswerCardDefaultExampleState
extends State<MultiAnswerCardDefaultExample> {
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 Component.element(
tag: 'div',
styles: Styles(raw: {'display': 'flex', 'flex-direction': 'column'}),
children: [
MultiAnswerCard<String>(
value: 'a',
selected: _selected.contains('a'),
onChanged: _toggle,
child: Text('Answer A'),
),
Gap.space8(),
MultiAnswerCard<String>(
value: 'b',
selected: _selected.contains('b'),
onChanged: _toggle,
child: Text('Answer B'),
),
],
);
}
}
class MultiAnswerCardDefaultExample extends StatefulWidget {
const MultiAnswerCardDefaultExample({super.key});
@override
State<MultiAnswerCardDefaultExample> createState() =>
_MultiAnswerCardDefaultExampleState();
}
class _MultiAnswerCardDefaultExampleState
extends State<MultiAnswerCardDefaultExample> {
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,
children: [
MultiAnswerCard<String>(
value: 'a',
selected: _selected.contains('a'),
onChanged: _toggle,
child: const Text('Answer A'),
),
const Gap.space8(),
MultiAnswerCard<String>(
value: 'b',
selected: _selected.contains('b'),
onChanged: _toggle,
child: const Text('Answer B'),
),
],
);
}
}
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',
);
}
}
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,
),
),
],
);
}
}
사용 시기 (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#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | T | — | 카드가 나타내는 값 (필수) |
selected | bool | false | 선택 상태 |
enabled | bool | true | 상호작용 가능 여부 |
onChanged |
void Function(T)? |
null |
카드 탭 시 콜백 (value 전달) |
child | Widget / 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)#
인터랙션#
- 클릭/탭:
onChanged에value전달 - 선택 상태: 두꺼운 테두리(
primary) + 선택 배경 - 호버: 미선택 카드는 hover 배경색 적용
enabled가false이면 비활성 (커서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)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | MultiAnswerCard | MultiAnswerCard |
| 전환 애니메이션 | AnimatedContainer | CSS transition-all |
| 호버 | MouseRegion |
mouseenter / mouseleave |
관련 컴포넌트 (Related Components)#
- MultipleChoice: 그룹 단위 선택 관리
- Checkbox: 단순 체크박스