ResponsiveContainer#
ResponsiveContainer는 자식 콘텐츠에 최대 너비 제약 + 수평 중앙 정렬 + 기본 수평 패딩을 적용하는 레이아웃 래퍼입니다. Tailwind의
container 유틸리티, Flutter의 ConstrainedBox + Center 패턴을 한 번에 제공합니다. 긴 텍스트 영역, 페이지 본문, 카드 그리드 등의 바깥 래퍼로 사용합니다.
Live Preview#
class ResponsiveContainerDefaultExample extends StatelessComponent {
const ResponsiveContainerDefaultExample({super.key});
@override
Component build(BuildContext context) {
final cs = context.theme.colorScheme;
return ResponsiveContainer(
maxWidth: CoreResponsiveContainerMaxWidth.sm,
child: Card(
cardStyle: const CoreCardStyle(padding: CoreSpace.space16),
child: div(
classes: 'text-${CoreTextStyles.bodyMedium.name} text-${cs.onSurface}',
[
Text(
'This content is centred and capped at the "sm" breakpoint '
'(640 px). Resize the preview to see the container stay '
'inside the max width with symmetric horizontal padding.',
),
],
),
),
);
}
}
class ResponsiveContainerDefaultExample extends StatelessWidget {
const ResponsiveContainerDefaultExample({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cs = theme.colorScheme;
return ResponsiveContainer(
maxWidth: CoreResponsiveContainerMaxWidth.sm,
child: Card(
cardStyle: const CoreCardStyle(
padding: EdgeInsets.all(CoreSpace.space16),
),
child: DefaultTextStyle.merge(
style: theme.typography.bodyMedium.toValue(theme: theme).copyWith(color: cs.onSurface.toValue()),
child: const Text(
'This content is centred and capped at the "sm" breakpoint '
'(640 px). Resize the preview to see the container stay '
'inside the max width with symmetric horizontal padding.',
),
),
),
);
}
}
class ResponsiveContainerChainExample extends StatelessComponent {
const ResponsiveContainerChainExample({super.key});
@override
Component build(BuildContext context) {
final cs = context.theme.colorScheme;
return ResponsiveContainer(
child: Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: div(
classes: 'text-${CoreTextStyles.bodyMedium.name} text-${cs.onSurface}',
[
Text(
'This content is centred and capped at the "sm" breakpoint '
'(640 px). Resize the preview to see the container stay '
'inside the max width with symmetric horizontal padding.',
),
],
),
),
).withStyle(
const CoreResponsiveContainerStyle(
maxWidth: CoreResponsiveContainerMaxWidth.sm,
centered: true,
horizontalPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
),
),
);
}
}
class ResponsiveContainerChainExample extends StatelessWidget {
const ResponsiveContainerChainExample({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cs = theme.colorScheme;
return ResponsiveContainer(
child: Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: DefaultTextStyle.merge(
style: theme.typography.bodyMedium.toValue(theme: theme).copyWith(color: cs.onSurface.toValue()),
child: const Text(
'This content is centred and capped at the "sm" breakpoint '
'(640 px). Resize the preview to see the container stay '
'inside the max width with symmetric horizontal padding.',
),
),
),
).withStyle(
const CoreResponsiveContainerStyle(
maxWidth: CoreResponsiveContainerMaxWidth.sm,
centered: true,
horizontalPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
),
),
);
}
}
사용 시기#
- 본문 영역이 넓은 모니터에서 가독성 있게 너비가 제한되어야 할 때
- 폼/카드 리스트/블로그 포스트의 바깥 경계
- 좌우 여백을 일관되게 유지하고 싶을 때 (
CoreSpace.space16기본)
기본 사용법#
ResponsiveContainer(
maxWidth: CoreResponsiveContainerMaxWidth.sm,
child: Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: Text('Centred, max-width content.'),
),
)
ResponsiveContainer(
maxWidth: CoreResponsiveContainerMaxWidth.sm,
child: Card(
child: div([Text('Centred, max-width content.').bodyMedium.onSurface]),
),
)
responsiveMaxWidth — tier별 max-width override#
정적인 maxWidth 하나만 쓰는 대신, Breakpoint의 3단계 tier(mobile/tablet/desktop)별로
서로 다른 max-width 프리셋을 지정할 수 있습니다. 예를 들어 mobile에서는 제약 없이(full) 화면을 꽉 채우고, tablet부터 점점 좁혀가는 레이아웃을 만들 때 유용합니다.
ResponsiveContainer(
// 정적 maxWidth는 responsiveMaxWidth가 지정하지 않은 tier의 폴백으로 쓰입니다.
maxWidth: CoreResponsiveContainerMaxWidth.lg,
responsiveMaxWidth: const {
CoreBreakpointTier.mobile: CoreResponsiveContainerMaxWidth.full,
CoreBreakpointTier.tablet: CoreResponsiveContainerMaxWidth.md,
CoreBreakpointTier.desktop: CoreResponsiveContainerMaxWidth.xl,
},
child: Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: Text('Max-width switches per breakpoint tier.'),
),
)
ResponsiveContainer(
// 정적 maxWidth는 responsiveMaxWidth가 지정하지 않은 tier의 폴백으로 쓰입니다.
maxWidth: CoreResponsiveContainerMaxWidth.lg,
responsiveMaxWidth: const {
CoreBreakpointTier.mobile: CoreResponsiveContainerMaxWidth.full,
CoreBreakpointTier.tablet: CoreResponsiveContainerMaxWidth.md,
CoreBreakpointTier.desktop: CoreResponsiveContainerMaxWidth.xl,
},
child: Card(
child: div([
Text('Max-width switches per breakpoint tier.').bodyMedium.onSurface,
]),
),
)
우선순위는 responsiveMaxWidth[현재 tier] > maxWidth > 디자인 시스템 기본값(xl)입니다 — 맵에 현재 tier가 없으면 정적
maxWidth로 개별 폴백하고(부분(partial) 맵 허용, Breakpoint의 boundaries가 tier별로 개별 폴백하는 것과 동일한 패턴),
responsiveMaxWidth 자체가 null(기본값)이면 breakpoint 관측 자체가 생략되어 기존 정적 maxWidth
동작이 그대로 유지됩니다 — non-breaking 확장입니다.
Props#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child |
Widget / Component |
— | 래핑할 자식 (required) |
maxWidth |
CoreResponsiveContainerMaxWidth? |
xl |
최대 너비 프리셋 (
sm
=640 /
md
=768 /
lg
=1024 /
xl
=1280 /
xxl
=1536 /
full
=제약 없음)
|
responsiveMaxWidth |
Map<CoreBreakpointTier, CoreResponsiveContainerMaxWidth>? |
null (정적 maxWidth 유지) |
tier별 max-width override. 누락된 tier는 개별적으로 maxWidth로 폴백 |
centered |
bool? |
true |
가로 중앙 정렬 여부. false이면 부모 왼쪽에 붙음 |
padding |
CoreEdgeInsets? |
CoreSpace.space16 (수평) |
수평 패딩. Flutter / Web 동일하게 CoreEdgeInsets |
responsiveContainerStyle |
CoreResponsiveContainerStyle? |
null |
위 필드를 프로젝트·테마 단위로 override하는 슬롯 |
CoreResponsiveContainerStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
maxWidth |
CoreResponsiveContainerMaxWidth? |
Max-width preset. |
centered |
bool? |
Whether the container is horizontally centred inside its parent. |
horizontalPadding |
CoreEdgeInsets? |
Horizontal padding. |
동작#
- viewport ≥ maxWidth: 컨테이너는
maxWidth제약으로 중앙에 고정, 좌우 여백은 균등 - viewport < maxWidth: 컨테이너는 부모 전체 너비를 차지, 수평 패딩이 그대로 적용
maxWidth: .full— 제약 없음, 그냥 부모 너비 그대로 + 수평 패딩-
responsiveMaxWidth가 지정되면 위 판정 전에 현재CoreBreakpointTier(Breakpoint와 동일한resolveBreakpoint관측)로 먼저 유효maxWidth프리셋을 선택합니다
플랫폼 차이#
-
Flutter:
ConstrainedBox(maxWidth)+SizedBox(width: infinity)+Padding+ (옵션)Center.maxWidth는theme.scaling을 곱해 논리 픽셀로 확대됨 -
Web:
div+w-full+mx-auto+max-w-screen-{size}Tailwind 클래스. 수평 패딩은var(--coui-space-{n})CSS 변수로 주입되어 브라우저 text-scaling에 자동 반응
두 플랫폼 모두 동일한 CoreBreakpoint 값(640/768/1024/1280/1536)에 결합되어 픽셀 폭이 동일합니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
콘텐츠 성격에 맞는 maxWidth 프리셋 선택
// 텍스트 위주 폼/본문 — 가독성을 위해 좁게
ResponsiveContainer(
maxWidth: CoreResponsiveContainerMaxWidth.sm,
child: form,
)
// 카드 그리드/대시보드 — 기본값(xl)보다 넓게
ResponsiveContainer(
maxWidth: CoreResponsiveContainerMaxWidth.xxl,
child: cardGrid,
)
기본값 xl(1280px)은 일반적인 페이지 폭에 맞춰져 있지만, 긴 텍스트 줄 길이를 줄여야 하는 폼/본문에는 sm/md가, 카드 그리드처럼 가로 공간이 많이 필요한 콘텐츠에는 xl보다 넓은 프리셋이 더 잘 맞습니다.
❌ Don't#
centered: false로 두고 중앙 정렬된 페이지 본문을 기대하지 않기
// ❌ centered: false면 부모 왼쪽에 붙어 좌측 정렬된 컬럼처럼 보임
ResponsiveContainer(
maxWidth: CoreResponsiveContainerMaxWidth.md,
centered: false,
child: pageBody,
)
centered(기본값 true)를 끄면 컨테이너가 maxWidth로 제약되되 부모의 왼쪽 가장자리에 붙습니다 — 넓은 화면에서 중앙 정렬된 본문을 원한다면 centered를 명시적으로 끄지 마세요.
접근성 (Accessibility)#
역할 / Semantics#
양 플랫폼 모두 아무 역할도 내보내지 않습니다. Flutter는 ConstrainedBox > SizedBox
> Padding(옵션으로 Align)만 반환하고 Semantics 호출이 없으며, Web은 <main>/<section>
같은 시맨틱 요소가 아니라 평범한 <div>를 렌더하고 호출자가 넘긴 attributes만 통과시킵니다.
읽는 입장에서 이것은 max-width · 중앙 정렬 · 패딩만 담당하는 투명한 상자라는 뜻입니다. 페이지 셸로 쓰더라도 랜드마크 목록에는 나타나지 않으므로, 이 영역을 랜드마크로 만들 책임은 호출자에게 있습니다.
키보드#
처리하는 키가 없습니다.
포커스#
포커스를 받지 않습니다. Flutter에 FocusNode/Focus/FocusScope가 없고 Web에
tabindex가 없어 탭 순서에 끼어들지 않습니다. "반응형"이라는 이름과 달리 tier 판정은 빌드 시점의 resolveBreakpoint
조회일 뿐, 인터랙티브 표면이 아닙니다.
스크린 리더#
스스로는 아무것도 읽히지 않고 리더는 곧장 child로 통과합니다. 브레이크포인트 tier가 바뀌어 max-width가 달라져도 아무런 안내가 없습니다
— 레이아웃 변경은 시각적으로만 일어납니다.
알려진 제약#
-
랜드마크 시맨틱이 없습니다. 페이지 본문 래퍼로 쓴다면 랜드마크는 직접 붙여야 합니다. Web은
attributes: {'role': 'main'}패스스루로 주입할 수 있지만, Flutter 생성자에는attributes/시맨틱 파라미터가 아예 없어 호출자가 바깥에서Semantics로 감싸야 합니다 — 이 비대칭은 코드 그대로입니다. - 포커스 관리·키보드 조작·레이아웃 변경 안내가 전부 없습니다.
전역으로 보장되는 항목(동작 줄이기·고대비·색 강제 모드 등)은 전역 접근성 축을 참고하세요.
레거시 vs 통일 비교 (Migration Notes)#
이전 버전의 Web CoUI 를 참조하는 코드에는 이 컴포넌트가 Container 라는 이름이었을 수 있습니다. Flutter 자체 위젯인 Container
와 이름이 겹쳐 프레임워크 심볼과 충돌하므로, Web 도 Flutter-first 원칙에 따라 ResponsiveContainer 로 개명되었습니다.
| 항목 | 레거시 (구 Web Container) | 통일 ResponsiveContainer |
|---|---|---|
| API | 동일 named properties | 동일 named properties |
| 이름 충돌 | Flutter/Dart Container 와 겹침 |
겹치지 않음, 반응형 max-width 성격을 이름에 명시 |
마이그레이션: Container(...) → ResponsiveContainer(...) — 파라미터는 그대로이므로 클래스명만 바꾸면 됩니다.