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

Card

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

목차

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

기본 사용법 (Basic Usage)#

// 기본 카드 (filled, 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('눌러 보세요'),
)

Live Preview#

구조 (Anatomy)#

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

부분역할슬롯
media상단 미디어media
overlaymedia 위overlay
header헤더header
body본문body
footer푸터footer
child슬롯 대신 쓰는 커스텀 자식child

사용 시기 (When to Use)#

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

  • 관련 정보를 하나의 단위로 묶어 표시할 때 (프로필, 상품, 게시물 등)
  • 시각적으로 콘텐츠를 구분하고 계층을 만들 때
  • 클릭 가능한 콘텐츠 블록이 필요할 때

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

  • OutlinedContainer: 카드 콘텐츠 슬롯 없이 저수준 표면 chrome만 조립할 때
  • Stat: 자유 콘텐츠 묶음 대신 지표 값과 변화량을 표시할 때
  • Hero: 항목 카드가 아니라 페이지의 인트로 섹션이 필요할 때
  • Accordion: 접고 펼 수 있는 콘텐츠 섹션이 필요할 때
  • Table: 구조화된 데이터를 행과 열로 표시할 때
  • Dialog: 본문 흐름의 표면 대신 콘텐츠를 모달 오버레이로 표시할 때

Props / Parameters#

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

속성타입기본값설명
variant CoreCardVariant .filled 시각 변형 (filled / outlined)
media W? null 상단 미디어 영역 (이미지 등)
headerW?null헤더 영역
bodyW?null본문 영역
footerW?null푸터 영역
overlay W? null media 위에 얹는 오버레이
childW?null커스텀 자식 (슬롯 대신)
onTap VoidCallback? null 탭/클릭 핸들러
role CoreSurfaceRole content (defaultRole) 표면의 콘텐츠/기능 레이어 역할. ARIA 역할이 아님
cardStyle CoreCardStyle? null chrome + 치수 오버라이드 묶음 (아래 표 참고)

CoreCardStyle 필드#

필드타입설명
surfaceStyle CoreSurfaceStyleId? Material override for this card; null inherits the surrounding material. The material — translucency, backdrop blur, chroma lift, backdrop dimming, edge sheen and refraction — is a theme axis that reaches every surface at once. A value here replaces it for this card alone: [CoreSurfaceStyleId.defaultStyle] is an opaque, stroke-bounded card inside a glass theme, [CoreSurfaceStyleId.liquidGlass] a glass card inside an opaque one. Palette, typography, geometry and density stay the surrounding theme's. [surfaceOpacity] and [surfaceBlur] tune the fill and the blur within whichever material applies. They cannot remove one, because neither reaches the sheen, the chroma lift or the backdrop dimming. Deliberately no default: null inherits the active theme material axis; a fixed default would override that axis before the resolver sees it.
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
borderColoroutlineVariant (fallback: outline)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',
    );
  }
}

스타일 시스템 (Style System)#

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),
        ),
      },
    ),
  ),
)

유리 테마 안의 불투명 카드 (Material Override)#

liquidGlass 프리셋에서는 모든 표면이 같은 유리 재질을 받습니다. 목록 안의 카드처럼 내용이 많은 면을 불투명하게 두려면 프리셋 전체가 아니라 그 카드의 재질만 cardStyle.surfaceStyle 로 지정합니다. defaultStyle 은 불투명한 기본 재질입니다 — 반투명 채움·배경 블러·가장자리 빛띠·굴절(그리고 앱이 켠 서리·몸체 음영)이 모두 빠지고, 평소의 외곽선이 그대로 남습니다. Flutter 와 Web 에서 같은 호출입니다.

Card(
  variant: CoreCardVariant.outlined,
  cardStyle: const CoreCardStyle(surfaceStyle: .defaultStyle),
  header: Text('주문 내역'),
  body: Text('불투명한 카드'),
)
  • 재질만 바뀝니다. 색 팔레트·타이포·모서리와 선 두께·밀도는 둘러싼 테마 그대로입니다.
  • null(기본)은 둘러싼 재질을 그대로 씁니다. 모든 카드에 적용하려면 CoreCardTheme.style 에 같은 값을 넣고, 한 장만 다시 바꾸려면 인스턴스 cardStyle 이 그 위에서 이깁니다.
  • 반대 방향도 같습니다. surfaceStyle: .liquidGlass 는 불투명 테마 안의 카드 한 장만 유리로 만듭니다.
  • surfaceOpacity / surfaceBlur 는 재질 안에서 채움과 블러를 조절하는 값입니다. 재질을 없애는 수단이 아닙니다 — 가장자리 빛띠·채도·배경 어둡힘은 두 값이 닿지 않습니다.
  • 접근성 설정(투명도 줄이기·고대비)은 이 오버라이드보다 항상 우선합니다.
  • 카드 안의 버튼·입력 같은 컨트롤은 자기 재질을 따로 정하지 않으면 둘러싼 재질을 받습니다. 불투명 카드 안에서도 그 컨트롤은 유리일 수 있습니다.

OutlinedContainer 는 같은 필드(containerStyle.surfaceStyle)를 받고, Card 는 그 값을 합성한 컨테이너에 그대로 넘깁니다.

변형 (Variants)#

Filled (기본)#

Card(
  variant: CoreCardVariant.filled,
  body: Text('배경색 채움'),
)

Outlined#

Card(
  variant: CoreCardVariant.outlined,
  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 애니메이션
  • child 가 있으면 media / header / body / footer 슬롯 대신 그 내용을 렌더합니다. 슬롯 경로의 overlay 는 media 위에만 그려지며 미디어가 없으면 표시하지 않습니다.
  • 자체 선택·닫기·비활성 플래그는 없습니다. onTap: null 은 표시 전용이며, 자식 버튼·링크의 동작은 그 자식이 소유합니다.

애니메이션#

  • 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)#

역할#

  • onTap 이 없을 때 — 역할 없음. Web 은 클래스만 실린 <div>, Flutter 는 OutlinedContainer 로 끝나고 Semantics 래핑이 없습니다. 카드는 자기 이름도 역할도 갖지 않는 순수 컨테이너입니다.
  • onTap 이 있을 때 — 카드 전체가 Clickable 로 감싸지고 역할은 그 Clickable 이 붙입니다. Web 은 role="button" + tabindex="0", Flutter 는 Semantics(button: true, enabled: true).
  • 이름을 받는 파라미터가 없습니다. 접근 가능한 이름은 header / body 등 슬롯에 넣은 텍스트에서 계산됩니다.
  • Web 은 caller attributes 를 컴포넌트 기본값보다 나중에 병합하므로 Card(attributes: {'role': 'article'}) 처럼 역할을 덮어쓸 수 있습니다. Flutter 에는 대응 진입점이 없어 호출부가 Semantics(...) 로 감싸야 합니다.

키보드#

onTap 이 없는 카드는 포커스를 받지 않습니다 — 아래는 전부 onTap 이 있을 때입니다.

키FlutterWeb
Tab카드 자신이 tab stop 하나동일 (tabindex="0")
EnteronTap 실행onTap 실행
Space onTap 실행 onTap 실행 (' ' / 'Spacebar')
화살표없음없음

카드 안쪽 요소에 포커스가 있을 때 Enter / Space 는 그 요소만 활성화하고 카드의 onTap 은 실행되지 않습니다 — Web 은 keydown 핸들러가 이벤트 타깃이 카드 루트 자신인지 확인하고, Flutter 는 안쪽 Clickable 의 ActivateIntent 가 먼저 처리합니다.

스크린 리더#

  • 이름의 출처는 슬롯 콘텐츠입니다. onTap 카드는 header·body·footer 텍스트가 이어져 버튼 이름이 되므로, 본문이 긴 카드에 onTap 을 걸면 이름도 그만큼 길어집니다.
  • 상태 변화를 알리는 live region 은 없습니다. hover shadow 같은 시각 변화는 통보되지 않습니다.
  • media 슬롯의 대체 텍스트에는 카드가 관여하지 않습니다 — 호출자가 넣은 위젯이 그대로 노출됩니다.

포커스 관리#

  • 진입 / 이탈 — 이동 없음. 카드는 오버레이가 아니라서 포커스를 옮기지 않습니다.
  • 트랩 — 없음. Tab 은 카드 밖으로 그대로 빠져나갑니다.
  • 포커스 표시 — onTap 카드는 :focus-visible 링을 그립니다 (Web 은 focus-visible:outline-* 유틸, Flutter 는 FocusOutline). 링과 카드 사이 2px 간격은 투명이라 카드가 놓인 배경이 그대로 보입니다.
  • onTap 카드 안에 포커스 가능한 요소가 있으면 tab stop 이 카드 하나 + 요소마다 하나로 늘어납니다.

알려진 제약#

  • onTap 카드 안에 버튼·링크를 두면 컨트롤이 중첩됩니다. Web 에서는 role="button" 안에 또 다른 버튼이 들어가고 리더는 두 개의 컨트롤로 읽습니다. 대안: 카드 전체가 아니라 내부 단일 액션에만 핸들러를 걸거나, onTap 카드 안에는 포커스 가능한 요소를 두지 않습니다.
  • filled variant 는 경계가 배경 톤 차이뿐입니다 — 보더 두께가 0, 보더 색이 투명이고 기본 elevation 이 none 입니다. OS 색 강제 모드에서 배경색이 시스템 색으로 치환되면 카드 경계가 사라집니다. Web 카드 루트에는 surface 마커가 붙어 있어 활성 표면 스타일이 강제-안전 경계를 선언한 경우에만 보더가 복원됩니다. 대안: variant: CoreCardVariant.outlined.
  • Web 은 포커스 링의 ringOpacity 를 반영하지 않습니다. 기본 경로는 알파 채널이 없는 Tailwind outline-* 클래스이고, 오버라이드 시 전환되는 scoped 규칙 경로도 ringOpacity 를 1 로 고정해 넘깁니다. Flutter 는 scaleAlpha 로 반영하므로 같은 Style 이 플랫폼마다 다르게 보입니다. 대안: 알파를 링 색 자체에 담아 focusOutlineStyle.borderColor 로 넘깁니다 — scoped 규칙이 색의 알파는 보존합니다.
  • 로딩 자리를 대신 그려주지 않습니다. loading 슬롯도 자체 스켈레톤도 없어, 값이 아직 없을 때는 호출자가 슬롯에 Skeleton 트리를 직접 넣어야 합니다 (header: isLoading ? Skeleton(...) : Text(title)).
  • RTL 에서 네 모서리가 서로 다른 borderRadius 는 뒤집히지 않습니다. CoreBorderRadius 는 물리 네 모서리라, 예컨대 왼쪽 두 모서리만 둥글게 준 값은 오른쪽-왼쪽 로케일에서도 그대로 왼쪽에 렌더됩니다. 기본값은 네 모서리가 같아 해당 없습니다. 대안: RTL 로케일에서 호출부가 좌우를 뒤집은 값을 직접 넘깁니다.

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

항목FlutterWeb
커스텀 자식child 콘텐츠DOM 커스텀 콘텐츠 래퍼
그림자 CoreCardElevation.* → BoxShadow CoreCardElevation.* → CSS shadow
표면 blur surfaceBlur → SurfaceBlur surfaceBlur → backdrop-filter: blur()
카드 한 장의 재질 surfaceStyle → 합성한 OutlinedContainer 가 그 재질을 그림 surfaceStyle → 카드 루트가 문서 마커 대신 coui-material-surface coui-material-<id> 를 달아 그 프리셋의 규칙만 선택됨
서브트리 단위 재질 SurfaceTheme 대응 없음 — 인스턴스 단위 surfaceStyle 만 있음
품질 적응 합성한 OutlinedContainer 를 통해 받음 카드 루트의 표면 마커를 통해 받음 — 스타일시트와 굴절 호스트가 같은 마커를 찾음

Web은 기존 호환을 위해 children: List<Component>? 추가 생성자 경로를 유지합니다. 목록이 child 보다 우선하며, 양 플랫폼에서 같은 호출을 쓰려면 child 에 조립한 콘텐츠를 넣습니다.

  • Accordion: 접고 펼 수 있는 섹션
  • Badge: Card 안에 상태 표시