DescriptionCard#
DescriptionCard는 라벨/값 쌍을 구조화된 메타데이터 요약으로 보여 줍니다.
주문 상세, 프로필, 관리 노트, 시험 결과처럼 사실을 한 장에 모아 둘 때 씁니다.
Live Preview#
class DescriptionCardDefaultExample extends StatelessComponent {
const DescriptionCardDefaultExample({super.key});
@override
Component build(BuildContext context) {
return 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),
],
);
}
}
class DescriptionCardDefaultExample extends StatelessWidget {
const DescriptionCardDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return 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),
],
);
}
}
class DescriptionCardFilledExample extends StatelessComponent {
const DescriptionCardFilledExample({super.key});
@override
Component build(BuildContext context) {
return const DescriptionCard(
title: 'Profile',
variant: CoreDescriptionCardVariant.filled,
rows: [
DescriptionRow(label: 'Name', value: 'Hong Gildong'),
DescriptionRow(label: 'Email', value: 'hong@example.com'),
DescriptionRow(label: 'Phone', value: '010-1234-5678'),
],
);
}
}
class DescriptionCardFilledExample extends StatelessWidget {
const DescriptionCardFilledExample({super.key});
@override
Widget build(BuildContext context) {
return const DescriptionCard(
title: 'Profile',
variant: CoreDescriptionCardVariant.filled,
rows: [
DescriptionRow(label: 'Name', value: 'Hong Gildong'),
DescriptionRow(label: 'Email', value: 'hong@example.com'),
DescriptionRow(label: 'Phone', value: '010-1234-5678'),
],
);
}
}
class DescriptionCardGhostExample extends StatelessComponent {
const DescriptionCardGhostExample({super.key});
@override
Component build(BuildContext context) {
return const DescriptionCard(
variant: CoreDescriptionCardVariant.ghost,
rows: [
DescriptionRow(label: 'Name', value: 'Kim Cheolsu'),
DescriptionRow(label: 'Email', value: 'kim@example.com'),
DescriptionRow(label: 'Phone', value: '010-1234-5678'),
],
);
}
}
class DescriptionCardGhostExample extends StatelessWidget {
const DescriptionCardGhostExample({super.key});
@override
Widget build(BuildContext context) {
return const DescriptionCard(
variant: CoreDescriptionCardVariant.ghost,
rows: [
DescriptionRow(label: 'Name', value: 'Kim Cheolsu'),
DescriptionRow(label: 'Email', value: 'kim@example.com'),
DescriptionRow(label: 'Phone', value: '010-1234-5678'),
],
);
}
}
class DescriptionCardVerticalExample extends StatelessComponent {
const DescriptionCardVerticalExample({super.key});
@override
Component build(BuildContext context) {
return const DescriptionCard(
title: 'Plant Care',
layout: CoreDescriptionCardLayout.vertical,
rowWeight: CoreDescriptionCardRowWeight.labelProminent,
rows: [
DescriptionRow(label: 'Species', value: 'Monstera Deliciosa'),
DescriptionRow(label: 'Watering', value: 'Every 7 days'),
DescriptionRow(label: 'Light', value: 'Indirect bright light'),
DescriptionRow(label: 'Temperature', value: '18°C – 27°C'),
],
);
}
}
class DescriptionCardVerticalExample extends StatelessWidget {
const DescriptionCardVerticalExample({super.key});
@override
Widget build(BuildContext context) {
return const DescriptionCard(
title: 'Plant Care',
layout: CoreDescriptionCardLayout.vertical,
rowWeight: CoreDescriptionCardRowWeight.labelProminent,
rows: [
DescriptionRow(label: 'Species', value: 'Monstera Deliciosa'),
DescriptionRow(label: 'Watering', value: 'Every 7 days'),
DescriptionRow(label: 'Light', value: 'Indirect bright light'),
DescriptionRow(label: 'Temperature', value: '18°C – 27°C'),
],
);
}
}
class DescriptionCardSpaceBetweenExample extends StatelessComponent {
const DescriptionCardSpaceBetweenExample({super.key});
@override
Component build(BuildContext context) {
return const DescriptionCard(
alignment: CoreDescriptionCardAlignment.spaceBetween,
showRowDividers: true,
rows: [
DescriptionRow(label: 'Product', value: 'AirPods Pro 2'),
DescriptionRow(label: 'Brand', value: 'Apple'),
DescriptionRow(label: 'Price', value: r'$369.00'),
DescriptionRow(label: 'Stock', value: '12'),
],
);
}
}
class DescriptionCardSpaceBetweenExample extends StatelessWidget {
const DescriptionCardSpaceBetweenExample({super.key});
@override
Widget build(BuildContext context) {
return const DescriptionCard(
alignment: CoreDescriptionCardAlignment.spaceBetween,
showRowDividers: true,
rows: [
DescriptionRow(label: 'Product', value: 'AirPods Pro 2'),
DescriptionRow(label: 'Brand', value: 'Apple'),
DescriptionRow(label: 'Price', value: '$369.00'),
DescriptionRow(label: 'Stock', value: '12'),
],
);
}
}
class DescriptionCardPlaceholderExample extends StatelessComponent {
const DescriptionCardPlaceholderExample({super.key});
@override
Component build(BuildContext context) {
return const DescriptionCard(
title: 'Empty Rows',
emptyValueBehavior: CoreDescriptionCardEmptyBehavior.placeholder,
emptyPlaceholder: 'Not provided',
rows: [
DescriptionRow(label: 'Name', value: 'Hong Gildong'),
DescriptionRow(label: 'Phone'),
DescriptionRow(label: 'Email'),
DescriptionRow(label: 'Address', value: 'Seoul, Gangnam-gu'),
],
);
}
}
class DescriptionCardPlaceholderExample extends StatelessWidget {
const DescriptionCardPlaceholderExample({super.key});
@override
Widget build(BuildContext context) {
return const DescriptionCard(
title: 'Empty Rows',
emptyValueBehavior: CoreDescriptionCardEmptyBehavior.placeholder,
emptyPlaceholder: 'Not provided',
rows: [
DescriptionRow(label: 'Name', value: 'Hong Gildong'),
DescriptionRow(label: 'Phone'),
DescriptionRow(label: 'Email'),
DescriptionRow(label: 'Address', value: 'Seoul, Gangnam-gu'),
],
);
}
}
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',
);
}
}
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,
),
),
),
],
);
}
}
사용 시기 (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#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
label | String | 필수 | 행 라벨 |
value | String? | 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. |
borderColor | CoreColor? | 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)#
| 필드 | filled | outlined | ghost |
|---|---|---|---|
backgroundColor | surface | surface | null |
borderWidth | 0 | stroke1 | 0 |
borderColor | — | outline | — |
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),
],
)
onTap과 row.onTap은 둘 다 Clickable로 합성되어, 함께 쓰면 카드 버튼 안에 행 버튼이 중첩되는 구조가 됩니다. 카드 전체 탭 또는 행 단위 탭 중 하나만 선택하세요.
접근성 (Accessibility)#
DescriptionCard 는 기본적으로 레이아웃 래퍼입니다 — 자체 역할도 이름도
내보내지 않고, 리더가 듣는 것은 전적으로 안에 놓인 텍스트입니다. onTap 또는
row.onTap 을 주는 순간에만 Clickable 이 합성되면서 버튼 시맨틱이 생깁니다.
역할 (Semantics)#
-
기본(비인터랙티브): 없습니다. Flutter 는
DecoratedBox, Web 은 평범한<div>로, 어느 쪽도Semantics/role을 emit 하지 않습니다. -
onTap또는row.onTap이 있을 때: 합성된Clickable을 통해서만 역할이 생깁니다 — FlutterSemantics(enabled:, button: true), Webrole="button"(비활성 시aria-disabled).
키보드#
컴포넌트 자신은 키를 처리하지 않습니다. 아래 키는 onTap / row.onTap 이 있어
Clickable 로 감싸졌을 때만 동작합니다.
| 키 | 동작 |
|---|---|
Enter | 카드(또는 행) 활성화 |
Space | 카드(또는 행) 활성화 |
방향키·Home/End 는 어느 경우에도 없습니다.
포커스#
비인터랙티브 상태에서는 포커스 가능한 요소가 없습니다 — FocusNode 도 tabindex
도 붙지 않습니다. 인터랙티브 상태의 포커스는 전부 Clickable 이 제공합니다:
Flutter 는 FocusNode + FocusOutline 링, Web 은 활성일 때
tabindex="0" +
브라우저 native :focus-visible 링. 포커스 트랩·복원·autofocus 는 없습니다.
스크린 리더#
- 비인터랙티브(기본): 역할도 그룹 노드도 없어서, 리더는 title 텍스트를 만난 뒤 label 과 value 텍스트가 평평하게 이어지는 흐름을 지나갑니다. 각 label 이 어느 value 에 속하는지를 알려주는 것은 아무것도 없습니다.
-
인터랙티브: 카드 전체(또는 그 행)가 하나의 버튼으로, 안쪽 텍스트를 이어
붙인 이름과 함께 읽힙니다. 비활성 상태는 Flutter
Semantics(enabled:)/ Webaria-disabled로 전달됩니다.
알려진 제약#
-
접근 가능한 이름을 넣을 파라미터가 없습니다 — 양 플랫폼 모두
semanticLabel/ariaLabel이 존재하지 않습니다. -
label ↔ value 가 프로그래밍적으로 연결되어 있지 않습니다.
<dl>/<dt>/<dd>도,aria-labelledby도,role="list"/"listitem"/"group"도 없습니다. 쌍 관계를 전달해야 하는 소비자는 직접 구성해야 합니다. -
title은 헤딩이 아니라 일반 텍스트입니다 — 헤딩 목록에 나타나지 않으므로 헤딩 탐색으로 건너뛸 수 없습니다. -
행 구분선과 title 구분선은
Divider로 그려지며,Divider자체가 separator 역할을 emit 하지 않습니다. -
onTap과row.onTap을 함께 주면 버튼 안에 버튼이 중첩됩니다. 둘 중 하나만 쓰는 편이 안전합니다. -
rowWeight/isEmphasized/emptyPlaceholder는 순수하게 시각적입니다 — 강조는 어떤 시맨틱 신호도 만들지 않고, 빈 값 자리의'-'는 그저 문자로 읽힐 뿐 "값 없음" 으로 전달되지 않습니다.
전역으로 적용되는 축(동작 줄이기·고대비·색 강제 모드·최소 터치 타겟)은 전역 접근성 축에 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
양쪽 모두 같은 DescriptionRow 데이터와 같은 Style 슬롯을 씁니다. 차이는
렌더 매체의 타입뿐입니다.
| 항목 | Flutter | Web |
|---|---|---|
| 행 데이터 클래스 | DescriptionRow | DescriptionRow |
| 색 오버라이드 | CoreColor → 렌더 시 Color |
CoreColor → 렌더 시 CSS 문자열 |
| 패딩 / 반경 | CoreEdgeInsets / CoreBorderRadius → Flutter 값 |
CoreEdgeInsets / CoreBorderRadius → CSS |
| 박스 그림자 | List<CoreShadowLayer> → List<BoxShadow> |
List<CoreShadowLayer> → CSS 문자열 |
행 onTap | 지원 | 지원 |
| 커스텀 값 위젯 | 지원 | 지원 |