HeroSection#
랜딩 페이지 최상단의 대형 히어로 블록입니다. 배경색 + 배경 이미지 + 반투명 오버레이 + 중앙 정렬 콘텐츠의 조합을 하나의 컴포넌트로 제공합니다.
Live Preview#
class HeroSectionDefaultExample extends StatelessComponent {
const HeroSectionDefaultExample({super.key});
@override
Component build(BuildContext context) {
final cs = context.theme.colorScheme;
return div(
[
HeroSection(
heroSectionStyle: const CoreHeroSectionStyle(minHeight: 280),
child: div(
[
div(
[Text('Welcome to CoUI')],
classes:
'text-${CoreTextStyles.titleLarge.name} font-${CoreFontWeight.scale.semibold} text-${cs.onSurface}',
),
div(
[
Text(
'Cross-platform design system for Flutter and Jaspr.',
),
],
classes:
'text-${CoreTextStyles.bodyMedium.name} text-${cs.onSurfaceVariant} text-center',
styles: const Styles(raw: {'margin-top': '12px'}),
),
],
classes: 'flex flex-col items-center',
),
),
],
// w-full 은 클래스로 — docs 의 폭 부여 룰(:has(.w-full))이
// 클래스만 매칭해서 inline width 로는 래퍼가 content-size 로 남는다.
classes: 'w-full',
styles: const Styles(raw: {'height': '280px'}),
);
}
}
class HeroSectionDefaultExample extends StatelessWidget {
const HeroSectionDefaultExample({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
height: 280,
child: HeroSection(
heroSectionStyle: const CoreHeroSectionStyle(minHeight: 280),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Welcome to CoUI',
style: theme.typography.titleLarge.toValue(theme: theme).copyWith(
color: theme.colorScheme.onSurface.toValue(),
fontWeight: FontWeight.w600,
),
),
SizedBox(height: CoreSpace.space12),
Text(
'Cross-platform design system for Flutter and Jaspr.',
textAlign: TextAlign.center,
style: theme.typography.bodyMedium.toValue(theme: theme).copyWith(
color: theme.colorScheme.onSurfaceVariant.toValue(),
),
),
],
),
),
);
}
}
class HeroSectionChainExample extends StatelessComponent {
const HeroSectionChainExample({super.key});
@override
Component build(BuildContext context) {
final cs = context.theme.colorScheme;
return div(
[
HeroSection(
child: div(
[
div(
[Text('Welcome to CoUI')],
classes:
'text-${CoreTextStyles.titleLarge.name} font-${CoreFontWeight.scale.semibold} text-${cs.onSurface}',
),
div(
[
Text(
'Cross-platform design system for Flutter and Jaspr.',
),
],
classes: 'text-${CoreTextStyles.bodyMedium.name} text-${cs.onSurfaceVariant} text-center',
styles: const Styles(raw: {'margin-top': '12px'}),
),
],
classes: 'flex flex-col items-center',
),
)
.withStyle(
const CoreHeroSectionStyle(
overlayColor: CoreColor.token(
CoreColors.primary,
opacity: CoreOpacity.opacity10,
),
minHeight: 280,
padding: CoreEdgeInsets.all(CoreSpace.space48),
),
)
.surfaceContainer,
],
// w-full 은 클래스로 — docs 의 폭 부여 룰(:has(.w-full))이
// 클래스만 매칭해서 inline width 로는 래퍼가 content-size 로 남는다.
classes: 'w-full',
styles: const Styles(raw: {'height': '280px'}),
);
}
}
class HeroSectionChainExample extends StatelessWidget {
const HeroSectionChainExample({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
height: 280,
child:
HeroSection(
child: Column(
mainAxisSize: .min,
children: [
Text(
'Welcome to CoUI',
style: theme.typography.titleLarge
.toValue(theme: theme)
.copyWith(
color: theme.colorScheme.onSurface.toValue(),
fontWeight: FontWeight.values.firstWhere(
(w) => w.value == CoreFontWeight.semiBold,
orElse: () => .w600,
),
),
),
SizedBox(height: CoreSpace.space12),
Text(
'Cross-platform design system for Flutter and Jaspr.',
textAlign: .center,
style: theme.typography.bodyMedium
.toValue(theme: theme)
.copyWith(
color: theme.colorScheme.onSurfaceVariant.toValue(),
),
),
],
),
)
.withStyle(
const CoreHeroSectionStyle(
overlayColor: CoreColor.token(
CoreColors.primary,
opacity: CoreOpacity.opacity10,
),
minHeight: 280,
padding: CoreEdgeInsets.all(CoreSpace.space48),
),
)
.surfaceContainer,
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 랜딩 페이지 상단의 대형 배너/히어로 영역
- 배경 이미지 위에 제목·설명·CTA를 중앙 정렬로 배치하고 싶을 때
대신 다른 컴포넌트를 사용하세요:
Banner: 짧은 공지/알림 바Card: 작은 콘텐츠 블록
기본 사용법 (Basic Usage)#
콘텐츠 · 배경 이미지 URL · 스크린리더 라벨만 위젯에 직접 넘기고, 배경색 · 오버레이 · 최소 높이 · 패딩은 heroSectionStyle 슬롯 하나로 흐릅니다.
HeroSection(
imageUrl: 'https://example.com/bg.jpg',
semanticLabel: 'CoUI 소개',
heroSectionStyle: const CoreHeroSectionStyle(
overlayColor: CoreColor.token(CoreColors.scrim),
padding: CoreEdgeInsets.all(CoreSpace.space48),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Welcome to CoUI').titleLarge.semiBold.onSurface,
const Text('Cross-platform design system.').bodyMedium.onSurface,
],
),
)
빠른 오버라이드 (Chain)#
이미 만든 HeroSection 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.surfaceContainer처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(surfaceContainer ==
CoreColors.surfaceContainer) 어느 컴포넌트에서 써도 뜻이 갈리지 않으며, 위 예시처럼 withStyle 뒤에 이어붙일 수도 있습니다.
class HeroSectionChainExample extends StatelessWidget {
const HeroSectionChainExample({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
height: 280,
child:
HeroSection(
child: Column(
mainAxisSize: .min,
children: [
Text(
'Welcome to CoUI',
style: theme.typography.titleLarge
.toValue(theme: theme)
.copyWith(
color: theme.colorScheme.onSurface.toValue(),
fontWeight: FontWeight.values.firstWhere(
(w) => w.value == CoreFontWeight.semiBold,
orElse: () => .w600,
),
),
),
SizedBox(height: CoreSpace.space12),
Text(
'Cross-platform design system for Flutter and Jaspr.',
textAlign: .center,
style: theme.typography.bodyMedium
.toValue(theme: theme)
.copyWith(
color: theme.colorScheme.onSurfaceVariant.toValue(),
),
),
],
),
)
.withStyle(
const CoreHeroSectionStyle(
overlayColor: CoreColor.token(
CoreColors.primary,
opacity: CoreOpacity.opacity10,
),
minHeight: 280,
padding: CoreEdgeInsets.all(CoreSpace.space48),
),
)
.surfaceContainer,
);
}
}
class HeroSectionChainExample extends StatelessComponent {
const HeroSectionChainExample({super.key});
@override
Component build(BuildContext context) {
final cs = context.theme.colorScheme;
return div(
[
HeroSection(
child: div(
[
div(
[Text('Welcome to CoUI')],
classes:
'text-${CoreTextStyles.titleLarge.name} font-${CoreFontWeight.scale.semibold} text-${cs.onSurface}',
),
div(
[
Text(
'Cross-platform design system for Flutter and Jaspr.',
),
],
classes: 'text-${CoreTextStyles.bodyMedium.name} text-${cs.onSurfaceVariant} text-center',
styles: const Styles(raw: {'margin-top': '12px'}),
),
],
classes: 'flex flex-col items-center',
),
)
.withStyle(
const CoreHeroSectionStyle(
overlayColor: CoreColor.token(
CoreColors.primary,
opacity: CoreOpacity.opacity10,
),
minHeight: 280,
padding: CoreEdgeInsets.all(CoreSpace.space48),
),
)
.surfaceContainer,
],
// w-full 은 클래스로 — docs 의 폭 부여 룰(:has(.w-full))이
// 클래스만 매칭해서 inline width 로는 래퍼가 content-size 로 남는다.
classes: 'w-full',
styles: const Styles(raw: {'height': '280px'}),
);
}
}
Props / Parameters#
HeroSection#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child |
Widget / Component |
필수 | 중앙에 배치되는 콘텐츠 위젯/컴포넌트 |
imageUrl |
String? |
null |
배경 이미지 URL (cover/center 렌더) |
semanticLabel |
String? |
null |
스크린리더 region label. 넘기지 않으면 landmark 처리가 플랫폼마다 갈립니다 (아래 접근성 절 참고) |
heroSectionStyle |
CoreHeroSectionStyle? |
null |
배경색·오버레이·최소 높이·패딩 단일 진입점 |
CoreHeroSectionStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
Background colour override. |
overlayColor |
CoreColor? |
Semi-transparent overlay colour drawn on top of the background image. No
default*
, deliberately — this field's absence is not a missing value, it is the absence of the overlay layer itself. Both platforms treat it as a whole-node condition rather than a colour: Flutter renders
Position.fill(ColoredBox(...))
only inside
if (resolved.overlayColor != null)
, and Web leaves
ResolvedHeroSection.overlay
null so the overlay
<div>
is never built (the one design-optional slot in that resolver — every other slot chrome is non-null). A default would therefore scrim every hero section in the tree, including the ones with no background image to scrim, and would add a DOM node to markup that does not have one. [backgroundColor] next to it is defaulted for the opposite reason: the background is always painted, so it always needs a value.
|
minHeight |
double? |
Minimum height in logical pixels. |
padding | CoreEdgeInsets? | Outer padding. |
Resolve chain#
CoreHeroSectionStyle.defaultX (static const)
→ CoreHeroSectionTheme.style // 프로젝트 공통
→ widget.heroSectionStyle // 인스턴스별
사용 가이드라인 (Usage Guidelines)#
✅ Do#
semanticLabel은 항상 넘기기
HeroSection(
imageUrl: 'https://example.com/bg.jpg',
semanticLabel: 'CoUI 소개',
child: content,
)
semanticLabel을 생략하면 Flutter는 아무 region도 만들지 않고 Web은 이름 없는 role="region"을 그대로 내보냅니다 — 같은 코드가 플랫폼마다 다른 접근성 트리를 만듭니다. 항상 넘기는 것이 유일하게 양쪽이 같아지는 사용법입니다.
❌ Don't#
정보가 담긴 이미지를 imageUrl 배경으로만 전달하지 않기
// ❌ 설명이 필요한 정보성 이미지를 배경으로만 전달
HeroSection(
imageUrl: 'https://example.com/quarterly-chart.png',
child: const Text('Q3 Results').titleLarge.onSurface,
)
imageUrl은 Flutter DecorationImage / Web CSS background-image로 그려져 보조기술에 노출되지 않습니다. 장식용 배경에는 맞지만, 정보를 담은 이미지는 child 안에 대체 텍스트가 있는 이미지 위젯으로 넣어야 합니다.
접근성 (Accessibility)#
이 절은 HeroSection 자신의 동작만 다룹니다. 전 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축을 참고하세요.
역할 / Semantics#
HeroSection 은 landmark region 을 내보내지만, 내보내는 조건이 플랫폼마다 다릅니다.
semanticLabel | Flutter | Web |
|---|---|---|
| 지정함 | CoUISemantics(role: .region, label:, container: true) |
루트 <div> 에 role="region" + aria-label |
| 생략 (기본값) | 아무것도 내보내지 않음 — 콘텐츠만 그대로 | role="region" 을 그대로 내보냄 (이름 없음) |
Flutter 가 라벨 없을 때 침묵하는 것은 의도된 선택입니다 — 이름 없는 region 은 아무 정보도 주지 못하고 SDK 가 이를 거부하기 때문에, 라벨이 없으면 landmark 대신 평범한 콘텐츠로 남깁니다. Web 에는 그 가드가 없습니다.
따라서 semanticLabel 을 항상 넘기는 것이 유일하게 양 플랫폼이 같아지는 사용법입니다.
키보드#
처리하는 키가 없습니다. 양 플랫폼 어디에도 키 핸들러가 없으며, Web 은 호출자가 넘긴 events 만 통과시킵니다.
포커스#
포커스와 관련된 구현이 전혀 없습니다. 히어로 자체는 포커스를 받지 않고 포커스를 관리하지도 않습니다. 포커스 가능한 요소는 전부 호출자가 child
로 넣은 콘텐츠(CTA 버튼 등)에서 옵니다.
스크린 리더#
semanticLabel 이 있으면 양 플랫폼 모두 이름 붙은 landmark region 으로 안내되어, 사용자가 landmark 목록에서 바로 건너뛸 수 있습니다. 라벨이 없으면 Flutter 는 조용하고, Web 은
이름 없는 region 이 landmark 목록에 익명 항목으로 남습니다.
배경 이미지는 <img> 가 아니라 Flutter DecorationImage(NetworkImage) / Web CSS
background-image 로 그려지므로 보조기술에 보이지 않습니다. 장식 이미지로서는 올바른 처리이지만, 정보를 담은 히어로 이미지는 설명할 경로가 없습니다.
알려진 제약#
-
라벨 없는 region 의 플랫폼 분기가 가장 실질적인 결함입니다 — 같은 호출이 Web 에서는 이름 없는 landmark 1개를, Flutter 에서는 0개를 만듭니다.
semanticLabel을 넘겨 해소하세요. -
heading 시맨틱이 없습니다.
child로 넣은 제목은 호출자가 직접 heading 으로 감싸지 않는 한 평범한 텍스트로 읽힙니다. aria-labelledby경로가 없습니다 — 이름은semanticLabel문자열로만 줄 수 있습니다.- 오버레이 레이어에는
aria-hidden이 없지만 내용이 비어 있어 추가로 읽히는 것은 없습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | HeroSection | HeroSection |
| child 타입 | Widget | Component |
| 배경 이미지 | DecorationImage(NetworkImage) + BoxFit.cover |
CSS
background-image: url(...)
+
background-size: cover; background-position: center
|
| overlay | Position.fill + ColoredBox |
<div class="absolute inset-0"> |
| 중앙 정렬 | Stacks + Center + Padding |
flex items-center justify-center |
레거시 vs 통일 비교 (Migration Notes)#
이전 버전의 Web CoUI 를 참조하는 코드에는 이 컴포넌트가 Hero 라는 이름이었을 수 있습니다. Flutter 쪽 이름(HeroSection)에 맞춰 Web 도
HeroSection 으로 개명되었습니다 — Flutter 자체의 Hero(페이지 전환 애니메이션 위젯)와 이름이 겹쳐 혼동을 주지 않기 위함이기도 합니다.
| 항목 | 레거시 (구 Web Hero) | 통일 HeroSection |
|---|---|---|
| API | 동일 named properties | 동일 named properties |
| 이름 충돌 | Flutter Hero(전환 애니메이션)와 겹침 | 겹치지 않음 |
마이그레이션: Hero(...) → HeroSection(...) — 파라미터는 그대로이므로 클래스명만 바꾸면 됩니다.