HeroSection | CoUI
LogoCoUI

HeroSection

배경 이미지·오버레이·중앙 정렬 콘텐츠를 가진 랜딩 페이지 히어로 영역

HeroSection#

랜딩 페이지 최상단의 대형 히어로 블록입니다. 배경색 + 배경 이미지 + 반투명 오버레이 + 중앙 정렬 콘텐츠의 조합을 하나의 컴포넌트로 제공합니다.

Live Preview#

사용 시기 (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.
paddingCoreEdgeInsets?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 을 내보내지만, 내보내는 조건이 플랫폼마다 다릅니다.

semanticLabelFlutterWeb
지정함 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)#

항목FlutterWeb
클래스명HeroSectionHeroSection
child 타입WidgetComponent
배경 이미지 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(...) — 파라미터는 그대로이므로 클래스명만 바꾸면 됩니다.

  • Card: 작은 콘텐츠 블록
  • Banner: 공지/알림 바