DescriptionCard | CoUI
LogoCoUI

DescriptionCard

라벨/값 쌍을 정리해 보여주는 메타데이터 요약 카드

DescriptionCard#

DescriptionCard는 라벨/값 쌍을 구조화된 메타데이터 요약으로 보여 줍니다. 주문 상세, 프로필, 관리 노트, 시험 결과처럼 사실을 한 장에 모아 둘 때 씁니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 주문·프로필·차량 이력처럼 라벨/값 쌍을 나열할 때
  • 구조화된 메타데이터를 한 카드에 모을 때
  • 합계/점수 행을 강조할 때 (isEmphasized: true)
  • 하단 액션 영역이 필요할 때 (bottomAction)

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

  • Table: 여러 레코드를 정렬·비교해야 할 때
  • Stat: 단일 KPI 값만 보여줄 때
  • Card: 자유 레이아웃이 필요할 때

빠른 오버라이드 (Chain)#

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

class DescriptionCardChainExample extends StatelessWidget {
  const DescriptionCardChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        const DescriptionCard(
          title: 'Order Detail',
          rows: [
            DescriptionRow(label: 'Order', value: 'ORD-2025-00391'),
            DescriptionRow(label: 'Item', value: 'AirPods Pro 2'),
            DescriptionRow(
              label: 'Total',
              value: '\$369.00',
              isEmphasized: true,
            ),
          ],
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
        const DescriptionCard(
          title: 'Order Detail',
          rows: [
            DescriptionRow(label: 'Order', value: 'ORD-2025-00391'),
            DescriptionRow(label: 'Item', value: 'AirPods Pro 2'),
            DescriptionRow(
              label: 'Total',
              value: '\$369.00',
              isEmphasized: true,
            ),
          ],
        ).withStyle(
          const CoreDescriptionCardStyle(
            backgroundColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            rowSpacing: CoreSpace.space12,
            padding: CoreEdgeInsets.symmetric(
              horizontal: CoreSpace.space24,
              vertical: CoreSpace.space8,
            ),
          ),
        ),
      ],
    );
  }
}
class DescriptionCardChainExample extends StatelessComponent {
  const DescriptionCardChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        const DescriptionCard(
          title: 'Order Detail',
          rows: [
            DescriptionRow(label: 'Order', value: 'ORD-2025-00391'),
            DescriptionRow(label: 'Item', value: 'AirPods Pro 2'),
            DescriptionRow(
              label: 'Total',
              value: r'$369.00',
              isEmphasized: true,
            ),
          ],
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
        const DescriptionCard(
          title: 'Order Detail',
          rows: [
            DescriptionRow(label: 'Order', value: 'ORD-2025-00391'),
            DescriptionRow(label: 'Item', value: 'AirPods Pro 2'),
            DescriptionRow(
              label: 'Total',
              value: r'$369.00',
              isEmphasized: true,
            ),
          ],
        ).withStyle(
          const CoreDescriptionCardStyle(
            backgroundColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            rowSpacing: CoreSpace.space12,
            padding: CoreEdgeInsets.symmetric(
              horizontal: CoreSpace.space24,
              vertical: CoreSpace.space8,
            ),
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

Props / Parameters#

여기에는 위젯 생성자 파라미터만 적습니다. 시각 chrome 은 descriptionCardStyle 슬롯 안에 있습니다.

DescriptionCard#

속성타입기본값설명
rows List<DescriptionRow> 필수 행 데이터 목록
size CoreDescriptionCardSize md 카드 크기 스케일 (xs / sm / md / lg / xl)
layout CoreDescriptionCardLayout horizontal 행 레이아웃 방향
variant CoreDescriptionCardVariant outlined 카드 스타일 (filled / outlined / ghost)
title String? null 선택적 제목 텍스트
titleLeading Widget? / Component? null 제목 앞에 그리는 위젯
titleTrailing Widget? / Component? null 제목 뒤에 그리는 위젯
showDivider bool true 제목 아래 구분선
showRowDividers bool false 행 사이 구분선
bottomAction Widget? / Component? null 하단 액션 영역
showBottomActionDivider bool true 하단 액션 위 구분선
alignment CoreDescriptionCardAlignment fixed 가로 정렬 (fixed 또는 spaceBetween)
valueOverflow CoreDescriptionCardOverflow wrap 긴 값 처리 (wrap 또는 ellipsis)
rowWeight CoreDescriptionCardRowWeight valueProminent 기본 강조 방향
emptyValueBehavior CoreDescriptionCardEmptyBehavior hide 빈 행 처리 (hide 또는 placeholder)
emptyPlaceholder String '-' 빈 행에 보여줄 자리 표시 텍스트
onTap VoidCallback? null 카드 탭 콜백
descriptionCardStyle CoreDescriptionCardStyle? null 인스턴스별 chrome · 치수 · 텍스트 슬롯 오버라이드 (배경 / 보더 / 반경 / 패딩 / 그림자 / 구분선 / 기본·강조 라벨·값 텍스트)

DescriptionRow#

속성타입기본값설명
labelString필수행 라벨
valueString?null행 값
valueWidget Widget? / Component? null 커스텀 값 위젯 — value보다 우선
leading Widget? / Component? null 라벨 앞에 그리는 위젯
trailing Widget? / Component? null 값 뒤에 그리는 위젯
onTap VoidCallback? null 행 탭 콜백
labelTextStyle CoreTextStyle? null 행 단위 라벨 텍스트 슬롯 오버라이드 ( fontSize / fontWeight / letterSpacing / color / lineHeight / fontFamily )
valueTextStyle CoreTextStyle? null 행 단위 값 텍스트 슬롯 오버라이드
isEmphasized bool false 이 행에 카드의 강조 라벨/값 텍스트 스타일 적용

스타일 시스템 (Style System)#

DescriptionCard 의 모든 chrome / dimensional / text-slot 오버라이드는 단일 descriptionCardStyle slot 으로 흐릅니다. variant 별 색(배경 / 보더)은 CoreDescriptionCardStyle.defaultsByVariant, size 별 치수는 defaultsBySize 가 단일 출처입니다.

CoreDescriptionCardStyle 필드#

필드타입설명
backgroundColor CoreColor? Card background fill colour.
borderColorCoreColor?Card border stroke colour.
borderWidth double? Card border stroke width (logical px, pre-scaling).
borderRadius CoreBorderRadius? Card border radius override.
padding CoreEdgeInsets? Card content padding override.
boxShadow List<CoreShadowLayer>? Card box shadow override. Deliberately without a default* : absence is what keeps the card flat. Both resolvers gate the shadow on this being set — Web emits no box-shadow property at all, Flutter hands BoxDecoration a null boxShadow — so a default lifts every description card off the page, and even an empty stack is not a no-op (it emits box-shadow: none on every card instead of emitting nothing). The elevation this card would cast if it were raised is a per-instance decision; only its tint is design-system-owned, which is why [shadowBaseColor] has a default and this does not.
shadowBaseColor CoreColor? Shadow base colour override — foundation of the elevation shadow stack. Defaults to [defaultShadowBaseColor] when unset.
dividerColor CoreColor? Divider line colour (title / inter-row / bottom-action separators inside the card).
titleColor CoreColor? Title slot text colour override. Defaults to [defaultTitleColor] when unset.
rowSpacing double? Vertical spacing between rows (logical px). Drives the Column -spacing-equivalent main-axis distance between consecutive rows. Stays a flat double — the rows wrapper consumes this scalar either via gap-${tailwindSpace} (Web) or Gap (Flutter).
columnGapStyle CoreGapStyle? Nested [CoreGapStyle] for the horizontal label↔value gap in horizontal layouts. Forwarded to Gap(gapStyle: …) (Flutter) / reflected as gap-x-${tailwindSpace} + inline rem (Web).
iconLabelGapStyle CoreGapStyle? Nested [CoreGapStyle] for the inline gap between a row's leading/trailing icon and the adjacent text. Forwarded to Gap(gapStyle: …) (Flutter); reflected as the title row's gap-${tailwindSpace} (Web).
labelValueGapStyle CoreGapStyle? Nested [CoreGapStyle] for the gap between label and value in the vertical layout. Forwarded to Gap(gapStyle: …) (Flutter) / reflected as gap-y-${tailwindSpace} + inline rem (Web).
titleContentGapStyle CoreGapStyle? Nested [CoreGapStyle] for the vertical spacing around the title divider and the bottom-action separator. Forwarded to Gap(gapStyle: …) . Defaults flow through [defaultsBySize].
rowLeadingGapStyle CoreGapStyle? Nested [CoreGapStyle] for the inline gap between a row's leading icon and its label. Forwarded to Gap(gapStyle: …) . Defaults to [defaultRowLeadingGapStyle] when unset.
labelTextStyle CoreTextStyle? Default label text slot style (per-row, before emphasis). Exposes the full typographic surface (fontSize / fontWeight / letterSpacing / color).
valueTextStyle CoreTextStyle? Default value text slot style (per-row, before emphasis).
titleTextStyle CoreTextStyle? Title text slot style.
emphasizedLabelTextStyle CoreTextStyle? Emphasised label text slot style (applied to rows with isEmphasized: true ). Deliberately without a default* : emphasis is carried by the row's value (see [emphasizedValueTextStyle]), and an emphasised row's label deliberately keeps the same style as every other label so the two columns still scan as columns. This slot exists for the caller who wants the label to move too; a default would restyle the label of every emphasised row in the kit.
emphasizedValueTextStyle CoreTextStyle? Emphasised value text slot style (applied to rows with isEmphasized: true ). Deliberately without a default* : the emphasis weight is applied by both resolvers as an auto-bold that fires only when neither this slot nor the row's own valueTextStyle states a fontWeight , and only when the row has a value (an empty-value placeholder stays unemphasised). A default carrying a weight would suppress that branch and bold unconditionally — including the placeholders it currently skips — and a default without one would be a value that changes nothing. The condition cannot be expressed as a field default because it reads the row, not the style.
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed Clickable (press scale / durations / focus ring / disabled opacity) used when the card / a row is tappable. Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the remaining defaults.

CoreDescriptionCardStyle 변형별 기본값 (CoreDescriptionCardVariantStyle)#

필드filledoutlinedghost
backgroundColorsurfacesurfacenull
borderWidth0stroke10
borderColoroutline

Resolve chain#

CoreDescriptionCardStyle.defaultsByVariant / defaultsBySize / defaultX
  → theme.descriptionCard.style              (project base)
  → widget.descriptionCardStyle               (per-instance)
  → row.labelTextStyle / row.valueTextStyle   (per-row overrides)

각 단계는 non-null 필드만 덮어쓰며, nested CoreTextStyle slot 도 재귀적으로 merge 됩니다 (fontSize 만 바꾸면 색상/굵기는 base 에서 상속).

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

✅ Do#

합계/강조 행에는 isEmphasized 사용

DescriptionRow(label: 'Total', value: r'$369.00', isEmphasized: true)

isEmphasized는 카드의 emphasizedLabelTextStyle / emphasizedValueTextStyle을 그 행에만 적용합니다 — 행마다 labelTextStyle / valueTextStyle을 직접 지정하지 않아도 강조가 일관되게 유지됩니다.


❌ Don't#

카드의 onTap과 행의 onTap을 동시에 지정하지 않기

// ❌ 카드 전체와 행이 모두 클릭 가능 — 버튼 안에 버튼이 중첩됨
DescriptionCard(
  onTap: openDetail,
  rows: [
    DescriptionRow(label: 'Order', value: 'ORD-2025-00391', onTap: openOrder),
  ],
)

onTaprow.onTap은 둘 다 Clickable로 합성되어, 함께 쓰면 카드 버튼 안에 행 버튼이 중첩되는 구조가 됩니다. 카드 전체 탭 또는 행 단위 탭 중 하나만 선택하세요.

접근성 (Accessibility)#

DescriptionCard 는 기본적으로 레이아웃 래퍼입니다 — 자체 역할도 이름도 내보내지 않고, 리더가 듣는 것은 전적으로 안에 놓인 텍스트입니다. onTap 또는 row.onTap 을 주는 순간에만 Clickable 이 합성되면서 버튼 시맨틱이 생깁니다.

역할 (Semantics)#

  • 기본(비인터랙티브): 없습니다. Flutter 는 DecoratedBox, Web 은 평범한 <div> 로, 어느 쪽도 Semantics / role 을 emit 하지 않습니다.
  • onTap 또는 row.onTap 이 있을 때: 합성된 Clickable 을 통해서만 역할이 생깁니다 — Flutter Semantics(enabled:, button: true), Web role="button" (비활성 시 aria-disabled).

키보드#

컴포넌트 자신은 키를 처리하지 않습니다. 아래 키는 onTap / row.onTap 이 있어 Clickable 로 감싸졌을 때만 동작합니다.

동작
Enter카드(또는 행) 활성화
Space카드(또는 행) 활성화

방향키·Home/End 는 어느 경우에도 없습니다.

포커스#

비인터랙티브 상태에서는 포커스 가능한 요소가 없습니다 — FocusNodetabindex 도 붙지 않습니다. 인터랙티브 상태의 포커스는 전부 Clickable 이 제공합니다: Flutter 는 FocusNode + FocusOutline 링, Web 은 활성일 때 tabindex="0" + 브라우저 native :focus-visible 링. 포커스 트랩·복원·autofocus 는 없습니다.

스크린 리더#

  • 비인터랙티브(기본): 역할도 그룹 노드도 없어서, 리더는 title 텍스트를 만난 뒤 label 과 value 텍스트가 평평하게 이어지는 흐름을 지나갑니다. 각 label 이 어느 value 에 속하는지를 알려주는 것은 아무것도 없습니다.
  • 인터랙티브: 카드 전체(또는 그 행)가 하나의 버튼으로, 안쪽 텍스트를 이어 붙인 이름과 함께 읽힙니다. 비활성 상태는 Flutter Semantics(enabled:) / Web aria-disabled 로 전달됩니다.

알려진 제약#

  • 접근 가능한 이름을 넣을 파라미터가 없습니다 — 양 플랫폼 모두 semanticLabel / ariaLabel 이 존재하지 않습니다.
  • label ↔ value 가 프로그래밍적으로 연결되어 있지 않습니다. <dl> / <dt> / <dd> 도, aria-labelledby 도, role="list" / "listitem" / "group" 도 없습니다. 쌍 관계를 전달해야 하는 소비자는 직접 구성해야 합니다.
  • title 은 헤딩이 아니라 일반 텍스트입니다 — 헤딩 목록에 나타나지 않으므로 헤딩 탐색으로 건너뛸 수 없습니다.
  • 행 구분선과 title 구분선은 Divider 로 그려지며, Divider 자체가 separator 역할을 emit 하지 않습니다.
  • onTaprow.onTap 을 함께 주면 버튼 안에 버튼이 중첩됩니다. 둘 중 하나만 쓰는 편이 안전합니다.
  • rowWeight / isEmphasized / emptyPlaceholder 는 순수하게 시각적입니다 — 강조는 어떤 시맨틱 신호도 만들지 않고, 빈 값 자리의 '-' 는 그저 문자로 읽힐 뿐 "값 없음" 으로 전달되지 않습니다.

전역으로 적용되는 축(동작 줄이기·고대비·색 강제 모드·최소 터치 타겟)은 전역 접근성 축에 있습니다.

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

양쪽 모두 같은 DescriptionRow 데이터와 같은 Style 슬롯을 씁니다. 차이는 렌더 매체의 타입뿐입니다.

항목FlutterWeb
행 데이터 클래스DescriptionRowDescriptionRow
색 오버라이드 CoreColor → 렌더 시 Color CoreColor → 렌더 시 CSS 문자열
패딩 / 반경 CoreEdgeInsets / CoreBorderRadius → Flutter 값 CoreEdgeInsets / CoreBorderRadius → CSS
박스 그림자 List<CoreShadowLayer>List<BoxShadow> List<CoreShadowLayer> → CSS 문자열
onTap지원지원
커스텀 값 위젯지원지원
  • Card: 자유 슬롯 레이아웃이 필요할 때
  • Stat: 단일 KPI 값만 강조할 때
  • Table: 여러 레코드를 행·열로 비교할 때