Card | CoUI
LogoCoUI

Card

슬롯 기반 레이아웃을 제공하는 카드 컨테이너 컴포넌트

Card#

관련 정보를 하나의 단위로 묶어 표시하는 컨테이너입니다. media / header / body / footer / overlay 슬롯을 제공합니다.

Live Preview#

사용 시기 (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 필드#

필드타입설명
paddingCoreEdgeInsets?Content padding override.
borderRadius CoreBorderRadius? Border radius override.
borderWidth double? Border width override (logical px, pre-scaling).
backgroundColor CoreColor? Background colour override.
borderColorCoreColor?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)#

필드outlinedfilled
backgroundColor surface (fallback: surfaceContainerLowest) surfaceContainer (fallback: surface)
borderWidthstroke10
borderColoroutline (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/heightsizing.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)#

항목FlutterWeb
커스텀 자식 child child + children (완전 커스텀 레이아웃)
그림자 CoreCardElevation.*BoxShadow CoreCardElevation.* → CSS shadow
표면 blur surfaceBlurSurfaceBlur surfaceBlurbackdrop-filter: blur()
  • Accordion: 접고 펼 수 있는 섹션
  • Badge: Card 안에 상태 표시