PageHeader | CoUI
본문으로 건너뛰기
LogoCoUI

PageHeader

페이지 머리 — 보조 설명 · 제목 · 설명 문장과 끝의 행동들

목차

PageHeader 는 페이지 맨 위의 머리입니다. 짧은 보조 설명(eyebrow) 아래 페이지 제목을, 그 아래 한 문장 설명을 쌓고, 행동(Button 들)을 줄 끝에 둡니다. 폭이 좁아지면 행동이 글 아래로 내려가고, 그래도 좁으면 행동끼리 줄을 바꿉니다 — 머리는 어떤 폭에서도 넘치지 않습니다.

기본 사용법 (Basic Usage)#

PageHeader(
  eyebrow: const Text('Workspace'),
  title: const Text('Settings'),
  description: const Text('Members, billing and integrations.'),
  actions: [
    Button(
      variant: .secondary,
      onPressed: () {},
      child: const Text('Cancel'),
    ),
    Button(onPressed: () {}, child: const Text('Save changes')),
  ],
)
PageHeader(
  eyebrow: const Text('Workspace'),
  title: const Text('Settings'),
  description: const Text('Members, billing and integrations.'),
  actions: [
    Button(
      variant: .secondary,
      onPressed: () {},
      child: const Text('Cancel'),
    ),
    Button(onPressed: () {}, child: const Text('Save changes')),
  ],
)

슬롯에 넘긴 Text 는 머리가 정한 역할 · 색으로 그려집니다 — 보조 설명 · 제목 · 설명의 글자 스타일을 따로 적지 않습니다. 폭은 부모가 정합니다 — 머리는 받은 폭을 채웁니다.

Live Preview#

구조 (Anatomy)#

글 묶음(보조 설명 · 제목 · 설명)과 행동 묶음 두 칸입니다. 글 묶음이 행동이 남긴 폭을 모두 차지합니다.

부분역할슬롯
eyebrow제목 위의 짧은 보조 설명 — 섹션 이름이나 상위 경로eyebrow
title 페이지 제목 — headingLevel 단계의 머리글 title
description제목 아래 한 문장description
actions줄 끝의 행동들 — 보통 Buttonactions

Figma: ❖ PageHeader

사용 시기 (When to Use)#

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

  • 설정 · 목록 · 대시보드 같은 페이지 본문 맨 위에 제목과 그 페이지의 행동을 둘 때
  • 제목 위에 섹션 이름을, 아래에 한 문장 설명을 함께 보여 줄 때

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

  • PanelHeader: 페이지 안의 패널 · 카드 하나의 머리 — 제목과 끝의 작은 행동 하나
  • AppBar: 화면 위에 고정되는 앱 바 — 뒤로 가기 · 메뉴 같은 탐색과 전역 행동

Props / Parameters#

W 는 Flutter의 Widget / Web의 Jaspr Component입니다.

속성타입기본값설명
eyebrow W? null 제목 위의 짧은 보조 설명. 없으면 그 줄을 그리지 않습니다
title W 필수 페이지 제목. headingLevel 단계의 머리글로 읽힙니다
description W? null 제목 아래 한 문장. 없으면 그 줄을 그리지 않습니다
actions List<W> const [] 줄 끝의 행동들. 비어 있으면 행동 묶음을 그리지 않습니다
headingLevel int CorePageHeaderContract.defaultHeadingLevel(1) 제목의 머리글 단계 1–6. 페이지 제목은 보통 1 입니다
pageHeaderStyle CorePageHeaderStyle? null 모든 chrome 이 지나는 단일 슬롯 (아래 표 참고)

CorePageHeaderStyle 필드#

필드타입설명
eyebrowTextStyle CoreTextStyle? Ambient text style of the eyebrow slot, merged on top of [defaultEyebrowTextStyle].
titleTextStyle CoreTextStyle? Ambient text style of the title slot, merged on top of [defaultTitleTextStyle].
descriptionTextStyle CoreTextStyle? Ambient text style of the description slot, merged on top of [defaultDescriptionTextStyle].
textSpacing double? Space between the lines of the text block — eyebrow, title and description, whichever are present. null → [defaultTextSpacing].
textActionsSpacing double? Space between the text block and the actions — across the row while they sit side by side, and down the column once the actions have moved under the text. null → [defaultTextActionsSpacing].
actionSpacing double? Space between two actions, and between two lines of them when they wrap. null → [defaultActionSpacing].
minTextWidth double? The narrowest the text block may get beside the actions. Below it the actions move under the text. null → [defaultMinTextWidth].

스타일 시스템 (Style System)#

pageHeaderStyle#

  • 시맨틱 / behaviour: 위젯 파라미터로 직접 (headingLevel)
  • chrome / dimensional: CorePageHeaderStyle 한 곳으로 (세 줄의 글자 스타일, 줄 사이 · 글과 행동 사이 · 행동 사이 간격, 글 묶음의 최소 폭)
부분역할색
eyebrow labelXl (15 · Medium) onSurfaceVariant
title headlineMedium (28 · Bold) onSurface
description bodyMedium (14) onSurfaceVariant

간격은 줄 사이(textSpacing) 4, 글 묶음과 행동 사이(textActionsSpacing) 8, 행동 사이(actionSpacing) 8 입니다. 세 간격은 간격 사다리 위의 값이라 밀도(CoreDensity)가 한 칸 옮깁니다. 글 묶음의 최소 폭(minTextWidth) 240 은 간격이 아니라 폭이라 밀도에 움직이지 않습니다.

Resolve chain

CorePageHeaderStyle.defaultX            // 기본값
  → CorePageHeaderTheme.style           // 프로젝트 공통
  → widget.pageHeaderStyle              // 인스턴스별

사용 예

CoreComponentTheme(
  pageHeader: CorePageHeaderTheme(
    style: CorePageHeaderStyle(
      titleTextStyle: CoreTextStyle.token(CoreTextStyles.headlineSmall),
    ),
  ),
)

빠른 오버라이드 (Chain)

이미 만든 PageHeader 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

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

  @override
  Widget build(BuildContext context) {
    return PageHeader(
      eyebrow: const Text('Workspace'),
      title: const Text('Settings'),
      actions: [Button(onPressed: () {}, child: const Text('Save changes'))],
    ).withStyle(
      const CorePageHeaderStyle(
        titleTextStyle: CoreTextStyle.token(CoreTextStyles.headlineSmall),
        textSpacing: CoreSpace.space8,
      ),
    );
  }
}
class PageHeaderChainExample extends StatelessComponent {
  const PageHeaderChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return PageHeader(
      eyebrow: const Text('Workspace'),
      title: const Text('Settings'),
      actions: [Button(onPressed: () {}, child: const Text('Save changes'))],
    ).withStyle(
      const CorePageHeaderStyle(
        titleTextStyle: CoreTextStyle.token(CoreTextStyles.headlineSmall),
        textSpacing: CoreSpace.space8,
      ),
    );
  }
}

동작 스펙 (Behavior)#

배치#

  • 나란히: 행동 묶음이 자기 폭(가장 넓을 때의 폭)을 차지하고, 글 묶음이 남은 폭을 모두 차지합니다. 둘은 아래 끝이 맞습니다 — 행동이 마지막 줄(설명이 있으면 설명)과 같은 높이에 앉습니다
  • 행동이 아래로: 글 묶음이 받을 폭이 minTextWidth(240)보다 좁아지면 행동 묶음이 글 아래로 내려가 시작 쪽에 붙습니다. 글 묶음과의 간격은 나란할 때와 같은 textActionsSpacing 입니다
  • 행동끼리 줄 바꿈: 한 줄 전체로도 행동이 다 들어가지 않으면 행동끼리 줄을 바꿉니다 (actionSpacing 이 가로 · 세로 간격). 머리는 넘치지 않습니다
  • 방향: 오른쪽에서 왼쪽으로 쓰는 로케일에서 두 배치가 함께 뒤집힙니다
  • 폭이 정해지지 않은 자리: 가로 스크롤 안처럼 폭이 정해지지 않은 자리에서는 두 플랫폼 모두 글 묶음을 내용 폭으로 잽니다 — 글이 minTextWidth 보다 짧으면 행동이 아래로 내려갑니다. 머리는 폭이 정해진 자리(페이지 열 · 패널)에 놓습니다

인터랙션#

  • 머리 자체는 입력을 받지 않습니다 — 행동의 동작은 각 행동(Button 등)이 가집니다

상태 전환#

  • 상태가 없습니다. 같은 슬롯과 같은 폭이면 같은 배치입니다

애니메이션#

  • 없습니다. 폭이 바뀌어 행동이 내려가거나 올라올 때도 즉시 바뀝니다

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

Do#

주 행동을 끝에 둔다

PageHeader(
  title: const Text('Settings'),
  actions: [
    Button(variant: .secondary, onPressed: cancel, child: const Text('Cancel')),
    Button(onPressed: save, child: const Text('Save changes')),
  ],
)

이유: 행동은 시작에서 끝 순서로 놓이고 탭 순서도 같습니다. 주 행동(primary)을 끝에 두면 나란할 때는 줄 끝에, 내려갔을 때는 마지막에 놓여 어느 배치에서도 같은 자리로 읽힙니다.

Don't#

패널 머리에 PageHeader 를 쓰지 않는다

// ❌ 페이지 안에 또 하나의 h1 이 생기고, 28 크기 제목이 패널을 누른다
Card(header: PageHeader(title: const Text('Recent files')))

// ✅ 패널 머리는 PanelHeader — 제목 18 · h2
Card(header: PanelHeader(title: const Text('Recent files')))

이유: 한 페이지에 머리글 1단계는 하나입니다. 패널은 그 아래 단계입니다.

접근성 (Accessibility)#

역할#

  • Flutter: 제목을 Semantics(headingLevel:) 로 감쌉니다 — 웹에서는 aria-level, Android 에서는 isHeading 으로 나갑니다. header: true 는 쓰지 않습니다 — 이 킷에서 header: 는 페이지 배너 랜드마크의 몫이라, 머리글마다 켜면 머리글이 배너로도 읽힙니다
  • Web: 제목이 <h1>…<h6> 요소(headingLevel)입니다. 브라우저 기본 여백은 지웁니다

키보드#

키FlutterWeb
Tab행동들 사이를 시작 → 끝 순서로행동들 사이를 시작 → 끝 순서로

머리 자체는 포커스를 받지 않습니다.

스크린 리더#

보조 설명 → 제목(머리글) → 설명 → 행동 순서로 읽힙니다. 제목만 머리글이고, 보조 설명과 설명은 일반 텍스트입니다. 상태 통보(live region)는 없습니다.

포커스 관리#

  • 진입 · 이탈 · 트랩: 없음 — 머리는 오버레이가 아닙니다

알려진 제약#

  • 머리글 단계는 호출자가 고릅니다. 페이지의 단계 구조(1 → 2 → 3)를 지키는 것은 호출자의 몫입니다

전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.

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

항목FlutterWeb
클래스명PageHeaderPageHeader
배치 렌더 오브젝트( HeaderWrapLayout )가 행동 묶음의 폭을 재고, 글 묶음에 남는 폭이 minTextWidth 보다 좁으면 행동을 아래로 내립니다 flex-wrap 줄 — 글 묶음의 flex-basis 가 minTextWidth , 하나의 gap 이 가로 · 세로 간격입니다
행동 줄 바꿈 Wrap(spacing, runSpacing) flex-wrap + gap
머리글 Semantics(headingLevel:) <h1>…<h6>
최소 폭의 단위논리 px × 레이아웃 배율rem — 브라우저 글자 배율을 따릅니다
크기 질의 IntrinsicWidth · IntrinsicHeight · Table 안에서도 답합니다 해당 없음 — 브라우저 배치
Web 전용 HTML 표면 — attributes · classes · css · eventHandlers · id 통과

위 차이 밖에는 없습니다 — 이름 · 순서 · 기본값을 두 위젯 생성자에서 대조했습니다.

  • PanelHeader: 페이지 안 패널의 머리 — 같은 배치 규칙
  • Button: actions 에 놓는 행동
  • AppBar: 화면 위의 고정 바
  • Scaffold: 머리가 놓이는 화면 구조

조합 예제#

Column(
  crossAxisAlignment: .stretch,
  spacing: CoreSpace.space24,
  children: [
    PageHeader(
      eyebrow: const Text('Workspace'),
      title: const Text('Settings'),
      actions: [Button(onPressed: save, child: const Text('Save changes'))],
    ),
    Card(
      header: PanelHeader(
        title: const Text('Members'),
        action: Button(
          variant: .ghost,
          size: .sm,
          onPressed: invite,
          child: const Text('Invite'),
        ),
      ),
      body: const Text('3 members'),
    ),
  ],
)