Card#
관련 정보를 하나의 단위로 묶어 표시하는 컨테이너입니다. media / header / body / footer
/ overlay 슬롯을 제공합니다.
Live Preview#
class CardDefaultExample extends StatelessComponent {
const CardDefaultExample({super.key});
@override
Component build(BuildContext context) {
return Card(
header: Text('Card Title'),
body: Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('Action'),
),
);
}
}
class CardDefaultExample extends StatelessWidget {
const CardDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return Card(
header: const Text('Card Title'),
body: const Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('Action'),
),
);
}
}
class CardFilledExample extends StatelessComponent {
const CardFilledExample({super.key});
@override
Component build(BuildContext context) {
return Card(
variant: CoreCardVariant.filled,
header: Text('Filled Card'),
body: Text('Filled variant with surface container background.'),
);
}
}
class CardFilledExample extends StatelessWidget {
const CardFilledExample({super.key});
@override
Widget build(BuildContext context) {
return const Card(
variant: CoreCardVariant.filled,
header: Text('Filled Card'),
body: Text('Filled variant with surface container background.'),
);
}
}
class CardElevatedExample extends StatelessComponent {
const CardElevatedExample({super.key});
@override
Component build(BuildContext context) {
return Card(
elevation: CoreCardElevation.medium,
header: Text('Elevated Card'),
body: Text('Card with medium shadow elevation.'),
);
}
}
class CardElevatedExample extends StatelessWidget {
const CardElevatedExample({super.key});
@override
Widget build(BuildContext context) {
return const Card(
elevation: CoreCardElevation.medium,
header: Text('Elevated Card'),
body: Text('Card with medium shadow elevation.'),
);
}
}
class CardChainExample extends StatelessComponent {
const CardChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Card(
header: Text('Card Title'),
body: Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('Action'),
),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderWidth/padding)까지.
Card(
header: Text('Card Title'),
body: Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('Action'),
),
).withStyle(
const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space24),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
backgroundColor: CoreColor.token(
CoreColors.surfaceContainerHighest,
),
borderColor: CoreColor.token(CoreColors.tertiary),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
class CardChainExample extends StatelessWidget {
const CardChainExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Card(
header: const Text('Card Title'),
body: const Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('Action'),
),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderWidth/padding)까지.
Card(
header: const Text('Card Title'),
body: const Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('Action'),
),
).withStyle(
const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space24),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
backgroundColor: CoreColor.token(
CoreColors.surfaceContainerHighest,
),
borderColor: CoreColor.token(CoreColors.tertiary),
),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 관련 정보를 하나의 단위로 묶어 표시할 때 (프로필, 상품, 게시물 등)
- 시각적으로 콘텐츠를 구분하고 계층을 만들 때
- 클릭 가능한 콘텐츠 블록이 필요할 때
대신 다른 컴포넌트를 사용하세요:
Accordion: 접고 펼 수 있는 콘텐츠 섹션이 필요할 때Table: 구조화된 데이터를 행과 열로 표시할 때Dialog: 임시 콘텐츠를 모달로 표시할 때
기본 사용법 (Basic Usage)#
// 기본 카드 (outlined, no elevation)
Card(
body: Text('카드 내용'),
)
// 슬롯 기반 레이아웃
Card(
header: Text('제목'),
body: Text('본문 내용입니다.'),
footer: Text('푸터'),
)
// variant + elevation (chrome / dimensional 은 cardStyle 로 통합)
Card(
variant: CoreCardVariant.filled,
cardStyle: CoreCardStyle(
elevation: CoreCardElevation.medium,
),
body: Text('강조된 카드'),
)
// 클릭 가능
Card(
onTap: () => print('clicked'),
body: Text('눌러 보세요'),
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
variant |
CoreCardVariant |
.outlined |
시각 변형 (filled / outlined) |
media |
Widget? / Component? |
null |
상단 미디어 영역 (이미지 등) |
header |
Widget? / Component? |
null |
헤더 영역 |
body |
Widget? / Component? |
null |
본문 영역 |
footer |
Widget? / Component? |
null |
푸터 영역 |
overlay |
Widget? / Component? |
null |
media 위에 얹는 오버레이 |
child |
Widget? / Component? |
null |
커스텀 자식 (슬롯 대신) |
children |
List<Component>? |
null |
완전 커스텀 레이아웃용 자식 목록 (Web 전용) |
onTap |
VoidCallback? |
null |
탭/클릭 핸들러 |
cardStyle |
CoreCardStyle? |
null |
chrome + 치수 오버라이드 묶음 (아래 표 참고) |
CoreCardStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
padding | CoreEdgeInsets? | Content padding override. |
borderRadius |
CoreBorderRadius? |
Border radius override. |
borderWidth |
double? |
Border width override (logical px, pre-scaling). |
backgroundColor |
CoreColor? |
Background colour override. |
borderColor | CoreColor? | Border colour override. |
boxShadow |
List<CoreShadowLayer>? |
Box shadow override. Takes precedence over [elevation]-derived shadow when set. |
headerBodyGapStyle |
CoreGapStyle? |
Gap style between the header and body slots — nested [CoreGapStyle] slot forwarded to the
Gap
widget that separates the two slots.
null
defers to [defaultHeaderBodyGapStyle].
|
bodyFooterGapStyle |
CoreGapStyle? |
Gap style between the body and footer slots — nested [CoreGapStyle] slot forwarded to the
Gap
widget that separates the two slots.
null
defers to [defaultBodyFooterGapStyle].
|
chromeAnimationDuration |
Duration? |
Chrome animation duration override (background / border / borderRadius / padding under theme change or per-instance style swap).
null
defers to [defaultChromeAnimationDuration].
|
hoverShadowAnimationDuration |
Duration? |
Hover-shadow animation duration override (AnimatedContainer wrapping the interactive surface).
null
defers to [defaultHoverShadowAnimationDuration].
|
width |
double? |
Fixed width override (logical px). Honoured when [sizing] is [CoreCardSizing.fixed].
Deliberately has no default* — absence is the design.
[sizing] is the axis that decides the geometry and it already has one ([defaultSizing] =
intrinsic
); this field is the operand the caller states for the
fixed
case, and only that case reads it. Null there still means "no measurement given": Flutter falls through to
IntrinsicWidth
on
width == null && height == null
, and the Web resolver writes the
width
rule only inside
if (merged.width != null)
. A constant would hand every
sizing: fixed
card a width nobody chose — a card that set only [height] would jump from auto to that number — and there is no design-system answer to "how wide is a card" to put here.
|
height |
double? |
Fixed height override (logical px). Honoured when [sizing] is [CoreCardSizing.fixed].
Deliberately has no default* — same reasoning as [width]
, and it shares the branch: Flutter's fall-through to
IntrinsicWidth
tests both fields at once, so a constant on either one alone puts every
fixed
card into a
SizedBox
. A card's height is its content's, which is why the two other sizing modes never read this field at all.
|
sizing |
CoreCardSizing? |
Sizing behaviour (intrinsic / expand / fixed). |
elevation |
CoreCardElevation? |
Shadow elevation level (none / soft / medium / strong). When [boxShadow] is also set, [boxShadow] wins. |
clipBehavior |
CoreClipMode? |
Clip behaviour override. |
bodyTextStyle |
CoreTextStyle? |
Body text style override (typography role + colour + weight in one
CoreTextStyle
).
null
defers to [defaultBodyTextStyle].
|
headerTextStyle |
CoreTextStyle? |
Header text style override (typography role + colour + weight).
null
defers to [defaultHeaderTextStyle].
|
overlayInset |
double? |
Overlay inset (logical px) — applied to the absolutely-positioned overlay slot.
null
defers to [defaultOverlayInset].
|
shadowBaseColor |
CoreColor? |
Shadow base colour override. null defers to [defaultShadowBaseColor]. |
hoverElevation |
CoreCardElevation? |
Hover elevation override (applied when an interactive card is hovered).
null
defers to [defaultHoverElevation].
|
surfaceBlur |
double? |
Backdrop blur sigma (logical px) for a glassmorphism surface — forwarded to the
OutlinedContainer
surface (Flutter
SurfaceBlur
/ Web
backdrop-filter: blur
).
null
/
0
= no blur. Opt-in: the design-system default cards are opaque.
|
surfaceOpacity |
double? |
Surface fill opacity (0.0–1.0) for a glassmorphism surface — lets the blurred backdrop show through the fill.
null
= fully opaque fill.
Deliberately has no
default*
— the baseline is a theme axis, and a
static const
is downstream of it.
The card raw-forwards this into the composed
OutlinedContainer
, whose resolver reads
merged.surfaceOpacity ?? theme.surfaceOpacity
: a constant here would always be non-null, so it would shadow the theme-wide surface treatment for cards specifically and put the value beyond the axis's reach — which
core/theme-axis-composition.md
forbids, since the axis's seat is token resolution and this class sits after it. The Web half additionally keys its emit on the null: it scales the background token's alpha only when this is set, and the resolver says why — an absent override has to stay null so
emitColor
still reads "no override" and leaves the background on its Tailwind token class rather than manufacturing an inline
rgb(… / a)
for every card.
|
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed
Clickable
on a tappable card (
onTap != null
) — press scale / durations / focus ring / disabled opacity. Merged on top of [defaultClickableStyle] (which pins
pressedScale: 1
) and raw-forwarded — the Clickable's own resolver fills the rest.
|
CoreCardStyle 변형별 기본값 (CoreCardVariantStyle)#
| 필드 | outlined | filled |
|---|---|---|
backgroundColor |
surface (fallback: surfaceContainerLowest) | surfaceContainer (fallback: surface) |
borderWidth | stroke1 | 0 |
borderColor | outline (fallback: outlineVariant) | transparent |
빠른 오버라이드 (Chain)#
이미 만든 Card 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 ==
CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class CardChainExample extends StatelessWidget {
const CardChainExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Card(
header: const Text('Card Title'),
body: const Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('Action'),
),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderWidth/padding)까지.
Card(
header: const Text('Card Title'),
body: const Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('Action'),
),
).withStyle(
const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space24),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
backgroundColor: CoreColor.token(
CoreColors.surfaceContainerHighest,
),
borderColor: CoreColor.token(CoreColors.tertiary),
),
),
],
);
}
}
class CardChainExample extends StatelessComponent {
const CardChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Card(
header: Text('Card Title'),
body: Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('Action'),
),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderWidth/padding)까지.
Card(
header: Text('Card Title'),
body: Text('This is the card body content.'),
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('Action'),
),
).withStyle(
const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space24),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
backgroundColor: CoreColor.token(
CoreColors.surfaceContainerHighest,
),
borderColor: CoreColor.token(CoreColors.tertiary),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
스타일 시스템 — cardStyle#
Card 의 모든 chrome / dimensional 오버라이드는 단일 cardStyle 필드(CoreCardStyle)
하나로 흐릅니다. width / padding / boxShadow
/ elevation 같은 값은 모두
CoreCardStyle 안의 nullable 필드이며, 위젯 생성자에는 chrome 파라미터가 없습니다.
Card(
variant: CoreCardVariant.outlined,
cardStyle: CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space24),
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
elevation: CoreCardElevation.soft,
sizing: CoreCardSizing.fixed,
width: 320,
),
header: Text('제목'),
body: Text('본문'),
)
Resolve chain#
cardStyle 은 다음 체인을 거쳐 최종 값으로 해석됩니다 (오른쪽이 이김):
design system default
→ CoreCardTheme.style // 프로젝트 공통
→ CoreCardTheme.variantStyles[variant] // variant 별 오버라이드
→ parent component slot override // 예: TimePicker.popoverPanelStyle
→ widget.cardStyle // 인스턴스별 오버라이드
각 레이어는 CoreCardStyle 의 어떤 필드든 부분적으로 채울 수 있습니다. null 인 필드는
다음 레이어로 넘겨주고, non-null 인 필드는 해당 레이어에서 확정됩니다.
테마 적용#
ThemeData.fromCore(
coreComponentTheme: CoreComponentTheme(
card: CoreCardTheme(
style: CoreCardStyle(
// 모든 카드에 적용되는 기본
borderRadius: CoreBorderRadius.all(CoreRadius.radius16),
padding: CoreEdgeInsets.all(CoreSpace.space20),
),
variantStyles: {
CoreCardVariant.filled: CoreCardStyle(
backgroundColor: CoreColor.token(CoreColors.primaryContainer),
),
},
),
),
)
변형 (Variants)#
Outlined (기본)#
Card(
variant: CoreCardVariant.outlined,
body: Text('외곽선만'),
)
Filled#
Card(
variant: CoreCardVariant.filled,
body: Text('배경색 채움'),
)
Elevation 단계#
Card(
cardStyle: CoreCardStyle(elevation: CoreCardElevation.none),
body: Text('평면'),
)
Card(
cardStyle: CoreCardStyle(elevation: CoreCardElevation.soft),
body: Text('약한 그림자'),
)
Card(
cardStyle: CoreCardStyle(elevation: CoreCardElevation.medium),
body: Text('중간 그림자'),
)
Card(
cardStyle: CoreCardStyle(elevation: CoreCardElevation.strong),
body: Text('강한 그림자'),
)
미디어 카드 (이미지 + 콘텐츠)#
Card(
media: Image.network('https://example.com/photo.jpg'),
header: Text('상품명'),
body: Text('상품 설명'),
footer: Text('가격'),
)
동작 스펙 (Behavior)#
인터랙션#
- 탭/클릭:
onTap설정 시 전체 카드 영역 클릭 가능 - 호버 (onTap 있을 때): 커서가 pointer 로 변경 + hover shadow 애니메이션
애니메이션#
-
chrome (배경/보더/반경/패딩) 전환:
cardStyle.chromeAnimationDuration(기본CoreDuration.normal= 200ms) -
hover shadow 전환:
cardStyle.hoverShadowAnimationDuration(기본CoreDuration.normal= 200ms)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
고정 크기가 필요하면 sizing: .fixed 와 함께 width/height 지정
Card(
cardStyle: CoreCardStyle(
sizing: CoreCardSizing.fixed,
width: 320,
height: 200,
),
body: Text('고정 크기 카드'),
)
width/height는 sizing이 .fixed일 때만 적용되는 값이라, sizing 없이 width만 주면 무시됩니다.
❌ Don't#
클릭 가능한 카드에서 onTap을 자식 위젯에만 걸지 않기
// ❌ body 안의 버튼에만 onTap — 카드 전체 클릭 영역을 놓침
Card(
body: GestureDetector(onTap: handleTap, child: Text('상세보기')),
)
// ✅ Card 자체의 onTap 사용 — 전체 영역 + hover shadow + cursor 파리티까지 포함
Card(
onTap: handleTap,
body: Text('상세보기'),
)
Card.onTap은 카드 전체를 클릭 영역으로 만들고 hover shadow 애니메이션과 커서 변경(cursor-pointer)까지 함께 처리합니다. 자식에만 핸들러를 걸면 클릭 가능 영역이 좁아지고 hover 피드백도 사라집니다.
접근성 (Accessibility)#
시맨틱#
-
Flutter: 기본은
Container.onTap이 있으면MouseRegion+GestureDetector로 감쌉니다. -
Web: 기본은
<div>.onTap이 있으면role="button",tabindex="0"을 붙입니다.
키보드#
onTap을 주면Enter/Space로 카드를 활성화할 수 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 커스텀 자식 | child |
child + children (완전 커스텀 레이아웃) |
| 그림자 | CoreCardElevation.* → BoxShadow |
CoreCardElevation.* → CSS shadow |
| 표면 blur | surfaceBlur → SurfaceBlur |
surfaceBlur → backdrop-filter: blur() |