AppBar | CoUI
LogoCoUI

AppBar

leading · 중앙(제목 또는 커스텀 child) · trailing 세 클러스터로 구성되는 통합 애플리케이션 바 컴포넌트

AppBar#

AppBar는 화면 상단(또는 하단)에 놓이는 통합 애플리케이션 바입니다. leading 클러스터, 중앙 영역(title / header / subtitle 세 슬롯의 세로 조합, 또는 이를 통째로 대체하는 child), trailing 액션 클러스터를 가로로 배치합니다. variant 하나로 바 모양을 고릅니다 — standard(64px 단일 행), compact(56px 단일 행), medium(112px — leading / trailing 이 놓인 48px 상단 행 아래에 큰 제목이 하단 정렬). 표면 색·높이·클러스터 간격·제목 타이포그래피·하단 헤어라인·elevated 그림자는 모두 단일 appBarStyle 슬롯을 지나며, Scaffoldheaders / footers 슬롯에 그대로 넣어 화면 헤더 / 푸터로 쓰는 것이 일반적인 사용법입니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 화면 제목과 뒤로 가기 · 설정 같은 액션을 한 줄에 담는 헤더가 필요할 때
  • Scaffoldheaders / footers 슬롯에 넣어 화면 골격의 헤더 / 푸터를 구성할 때
  • 큰 제목이 별도 행에 놓이는 두 행 헤더(medium)가 필요할 때

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

  • NavigationBar: 화면 헤더가 아니라 주요 섹션 간 이동 내비게이션이 필요할 때
  • Menubar: 데스크톱 스타일의 메뉴 트리거 바(File / Edit / View)가 필요할 때

기본 사용법 (Basic Usage)#

AppBar(
  leading: [
    Button(
      variant: CoreButtonVariant.plain,
      onPressed: () {},
      child: const Icon(LucideIcons.arrowLeft),
    ),
  ],
  title: const Text('Dashboard'),
  trailing: [
    Button(
      variant: CoreButtonVariant.plain,
      onPressed: () {},
      child: const Icon(LucideIcons.settings),
    ),
  ],
)
AppBar(
  leading: [
    Button(
      variant: CoreButtonVariant.plain,
      onPressed: () {},
      child: const Icon(LucideIcons.arrowLeft),
    ),
  ],
  title: const Text('Dashboard'),
  trailing: [
    Button(
      variant: CoreButtonVariant.plain,
      onPressed: () {},
      child: const Icon(LucideIcons.settings),
    ),
  ],
)

title 에 넣은 텍스트는 활성 variant 의 제목 타이포그래피(standard / compacttitleMedium, mediumtitleLarge)를 자동으로 상속하므로 Text('...') 만 넣으면 됩니다. leading / trailing 의 아이콘 버튼은 Button(variant: CoreButtonVariant.plain) 으로 합성합니다 — 투명 배경 · hover 페이드 · 포커스 링을 버튼이 스스로 그립니다.

빠른 오버라이드 (Chain)#

이미 만든 AppBar 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .surfaceContainer처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(surfaceContainer == CoreColors.surfaceContainer) 어느 컴포넌트에서 써도 뜻이 갈리지 않으며, 아래 예시처럼 withStyle 뒤에 이어붙일 수도 있습니다.

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

  @override
  Widget build(BuildContext context) {
    return AppBar(
          leading: [
            Button(
              variant: CoreButtonVariant.plain,
              onPressed: () {},
              child: const Icon(LucideIcons.arrowLeft),
            ),
          ],
          title: const Text('Dashboard'),
          trailing: [
            Button(
              variant: CoreButtonVariant.plain,
              onPressed: () {},
              child: const Icon(LucideIcons.settings),
            ),
          ],
        )
        .withStyle(
          const CoreAppBarStyle(
            dividerColor: CoreColor.token(CoreColors.outline),
            dividerWidth: CoreStrokeWidth.stroke2,
          ),
        )
        .surfaceContainer;
  }
}
class AppBarChainExample extends StatelessComponent {
  const AppBarChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return AppBar(
          leading: [
            Button(
              variant: CoreButtonVariant.plain,
              onPressed: () {},
              child: const Icon(LucideIcons.arrowLeft),
            ),
          ],
          title: const Text('Dashboard'),
          trailing: [
            Button(
              variant: CoreButtonVariant.plain,
              onPressed: () {},
              child: const Icon(LucideIcons.settings),
            ),
          ],
        )
        .withStyle(
          const CoreAppBarStyle(
            dividerColor: CoreColor.token(CoreColors.outline),
            dividerWidth: CoreStrokeWidth.stroke2,
          ),
        )
        .surfaceContainer;
  }
}

Props / Parameters#

속성타입기본값설명
title Widget? / Component? null 바의 주요 제목 콘텐츠 — 활성 variant 의 제목 타이포그래피를 상속
header Widget? / Component? null 제목 위 보조 콘텐츠
subtitle Widget? / Component? null 제목 아래 보조 콘텐츠
child Widget? / Component? null 커스텀 중앙 콘텐츠 — header / title / subtitle 열을 통째로 대체 ( title 과 동시 지정 불가)
leading List<Widget> / List<Component> [] leading(시작) 클러스터 — 보통 뒤로 가기 · 메뉴 아이콘 버튼 하나
trailing List<Widget> / List<Component> [] trailing(끝) 액션 클러스터
variant CoreAppBarVariant standard 바 모양 — standard (64px) / compact (56px) / medium (112px 두 행)
trailingExpanded bool false trailing 클러스터가 남은 너비를 채우고 액션을 끝에 정렬 (단일 행 variant 에서만 의미)
centerTitle bool false 제목(중앙 열)을 가운데 정렬
showDivider bool true 하단 헤어라인 디바이더 표시
elevated bool false elevated 드롭 섀도 표시
appBarStyle CoreAppBarStyle? null 모든 chrome 이 지나는 단일 슬롯 (아래 표 참고)

스타일 시스템 (Style System)#

CoreAppBarStyle 필드#

필드타입설명
backgroundColor CoreColor? Bar surface colour override. null defers to [defaultBackgroundColor].
padding CoreEdgeInsets? Horizontal bar inset override — the outer inset of the cluster row (the root for standard / compact, the top row for medium). null defers to [defaultPadding].
spacing double? Gap between the leading / title / actions clusters (logical px). Mirrors Flutter Row.spacing between the clusters. null defers to [defaultSpacing].
actionSpacing double? Gap between adjacent widgets inside the leading or trailing action cluster (logical px). Mirrors Flutter Row.spacing applied to each cluster. null defers to [defaultActionSpacing].
height double? Total bar height override (logical px). null defers to the active variant's entry in [defaultsByVariant].
topRowHeight double? Height of the medium variant's top row (logical px). Consumed only by the medium render branch. null defers to [defaultTopRowHeight].
titleStyle CoreTextStyle? Title typography override. Text colour is carried via CoreTextStyle.color inside this slot. null (or any unset field) defers to the active variant's entry in [defaultsByVariant].
titlePadding CoreEdgeInsets? Inset of the title block. null defers to the active variant's entry in [defaultsByVariant].
dividerColor CoreColor? Bottom hairline colour override. null defers to [defaultDividerColor].
dividerWidth double? Bottom hairline thickness override (logical px). null defers to [defaultDividerWidth].
elevatedShadow List<CoreShadowLayer>? Drop shadow shown while elevated is on. null defers to [defaultElevatedShadow].

CoreAppBarStyle 변형별 기본값 (CoreAppBarStyle)#

필드standardcompactmedium
heightsize64size56size112
titleStyle CoreTextStyle.token(CoreTextStyles.titleMedium, color: CoreCo… CoreTextStyle.token(CoreTextStyles.titleMedium, color: CoreCo… CoreTextStyle.token(CoreTextStyles.titleLarge, color: CoreCol…
titlePadding CoreEdgeInsets.symmetric(horizontal: CoreSpace.space8) CoreEdgeInsets.symmetric(horizontal: CoreSpace.space8) CoreEdgeInsets.only(left: CoreSpace.space16, right: CoreSpace…

Resolve chain#

appBarStyle 은 프로젝트 테마와 인스턴스 오버라이드를 한 줄로 합칩니다 — 뒤에 오는 층이 앞 층의 같은 필드를 덮습니다.

design system default (CoreAppBarStyle.defaultX / defaultsByVariant)
  → CoreAppBarTheme.style                    // 프로젝트 공통
  → CoreAppBarTheme.variantStyles[variant]   // 프로젝트 per-variant
  → widget.appBarStyle                       // 인스턴스별

titleStyle 은 중첩 슬롯이라 재귀 merge 됩니다 — CoreTextStyle(color: …) 처럼 색만 준 부분 오버라이드는 variant 의 role(titleMedium / titleLarge)을 그대로 두고 색만 바꿉니다.

위젯 슬롯 직접 주입 (asChild)#

chrome(색 · 간격 · 높이)은 appBarStyle 로 흐르지만, 무엇을 그릴지는 슬롯이 정합니다. title / header / subtitle / child / leading / trailing 은 전부 위젯 슬롯이라 어떤 위젯이든 그대로 받습니다 (shadcn asChild 패턴) — 제목의 typography role 자체를 바꾸고 싶다면 titleStyle 을 만지는 대신 title: Text('Inbox').titleLarge 처럼 이미 스타일이 붙은 위젯을 넣고, leading / trailing 버튼의 variant 를 바꾸고 싶다면 원하는 Button(variant: …) 을 직접 넣습니다. 슬롯에 넣은 위젯의 명시 스타일이 바의 기본 제목 타이포그래피보다 우선합니다.

프로젝트 테마#

CoreComponentTheme(
  appBar: CoreAppBarTheme(
    style: CoreAppBarStyle(
      backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
    ),
    variantStyles: {
      CoreAppBarVariant.medium: CoreAppBarStyle(
        titlePadding: CoreEdgeInsets.only(
          left: CoreSpace.space24,
          right: CoreSpace.space24,
          bottom: CoreSpace.space16,
        ),
      ),
    },
  ),
)

변형 (Variants)#

standard (기본)#

64px 단일 행. leading · 중앙 · trailing 이 한 줄에 놓이고 제목은 titleMedium.

AppBar(
  title: const Text('Dashboard'),
)

compact#

56px 단일 행. 밀도 높은 화면(툴 패널 · 사이드 시트 헤더)용이며 제목 타이포그래피는 standard 와 같습니다.

AppBar(
  variant: CoreAppBarVariant.compact,
  title: const Text('Notifications'),
)

medium#

112px 두 행. 48px 상단 행에 leading / trailing 이 놓이고, 그 아래 남은 공간에 titleLarge 제목이 하단 정렬됩니다. 제목 블록의 여백은 titlePadding (기본 좌우 16 · 하단 12) 이 정합니다.

AppBar(
  variant: CoreAppBarVariant.medium,
  leading: [
    Button(
      variant: CoreButtonVariant.plain,
      onPressed: () {},
      child: const Icon(LucideIcons.arrowLeft),
    ),
  ],
  title: const Text('Inbox'),
)

동작 스펙 (Behavior)#

레이아웃#

  • 단일 행 (standard / compact): [leading] [중앙 열] [trailing]spacing 간격으로 가로 배치. 중앙 열이 남은 너비를 차지하고 자기 titlePadding(기본 좌우 8) 만큼 안으로 들어가며, trailingExpanded: true 면 trailing 클러스터가 대신 남은 너비를 받아 액션을 끝에 정렬합니다.
  • 두 행 (medium): topRowHeight(기본 48px) 높이의 상단 행에 leading 과 trailing 이 양 끝으로 벌어지고, 남은 높이에 중앙 열이 titlePadding 안쪽 하단(centerTitle 이면 하단 가운데)에 정렬됩니다. 상단 행이 이미 양 끝 정렬이라 trailingExpanded 는 영향이 없습니다.
  • 중앙 열: headertitlesubtitle 순의 세로 열. child 를 주면 이 열 전체가 child 로 대체됩니다.
  • 클러스터 내부 간격: leading / trailing 안의 인접 위젯은 actionSpacing 으로 벌어집니다 — 클러스터 사이 간격(spacing)과 별개 필드라 한쪽만 넓힐 수 있습니다.

제목 정렬#

  • centerTitle: false (기본): 중앙 열이 시작 정렬(stretch) — 제목이 leading 바로 옆에서 시작
  • centerTitle: true: 중앙 열과 텍스트가 가운데 정렬

디바이더 · 그림자#

  • showDivider: true (기본): 바 하단에 dividerColor / dividerWidth (기본 outlineVariant · 1px) 헤어라인
  • elevated: true: elevatedShadow (기본 CoreShadow.sm) 드롭 섀도. 표면 채움이 항상 불투명한 surface 라 그림자가 바 아래로 비치지 않습니다.

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

✅ Do#

아이콘 액션은 Button(variant: CoreButtonVariant.plain) 으로 합성하기

AppBar(
  leading: [
    Button(
      variant: CoreButtonVariant.plain,
      onPressed: goBack,
      child: const Icon(LucideIcons.arrowLeft),
    ),
  ],
  title: const Text('Settings'),
)

plain 버튼이 투명 배경 · hover 페이드 · 포커스 링 · 터치 타겟을 스스로 그립니다. 맨 Icon 만 넣으면 클릭도 포커스도 되지 않습니다.

Scaffoldheaders / footers 슬롯에 넣기

Scaffold(
  headers: [
    AppBar(title: const Text('Dashboard')),
  ],
  child: body,
)

Scaffold 가 헤더 섹션을 본문 위에 쌓고, 플로팅 헤더 · 로딩 인디케이터와의 겹침을 처리합니다.


❌ Don't#

titlechild 를 동시에 주기

// ❌ assert 실패 — 두 슬롯은 서로 배타적이다
AppBar(
  title: const Text('Dashboard'),
  child: searchField,
)

childheader / title / subtitle 열을 통째로 대체하는 슬롯입니다. 검색 필드처럼 자유 형식 중앙 콘텐츠가 필요하면 child 만 주고, 제목이 필요하면 title 계열만 씁니다.

높이 · 색을 바깥 래퍼로 덮어쓰기

// ❌ 바가 자기 chrome 을 가진다 — 래퍼로 덮으면 variant · 테마 오버라이드와 어긋난다
SizedBox(
  height: 80,
  child: AppBar(title: const Text('Dashboard')),
)

높이 · 표면 색 · 간격은 전부 appBarStyle (또는 프로젝트의 CoreAppBarTheme) 로 흐릅니다. withStyle(const CoreAppBarStyle(height: CoreSize.size80)) 처럼 슬롯으로 주면 Web · Flutter 가 같이 움직입니다.

접근성 (Accessibility)#

스크린 리더#

  • Flutter: 바 자체는 시맨틱 래퍼를 추가하지 않습니다 — leading / trailing 의 ButtontitleText 가 각자의 시맨틱을 그대로 노출합니다. 읽기 순서는 leading → 중앙 열 → trailing.
  • Web: 루트가 <div> 이며 랜드마크 role 을 자동으로 붙이지 않습니다. 페이지 헤더로 쓰면 attributes: {'role': 'banner'} 를 넘겨 랜드마크로 노출하세요.

키보드 인터랙션#

바 자체는 포커스를 받지 않습니다. leading / trailing 의 Button 이 DOM · 위젯 순서대로 Tab 포커스를 받고, Enter / Space 로 활성화됩니다 — 포커스 링은 각 버튼이 그립니다.

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

항목FlutterWeb
클래스명AppBarAppBar
렌더 루트 DecoratedBox + SizedBox(height:) <div> (flex row · w-full)
하단 디바이더 Border(bottom:) border-b + px 고정 border-bottom-width
elevated 그림자 BoxDecoration.boxShadow inline box-shadow
HTML 배관 id / classes / css / attributes / eventHandlers 통과
  • Scaffold: headers / footers 슬롯에 AppBar 를 넣어 화면 골격을 구성
  • NavigationBar: 주요 섹션 이동 내비게이션 — 헤더 대신 쓰지 않고 함께 씁니다

조합 예제#

Scaffold(
  headers: [
    AppBar(
      leading: [
        Button(
          variant: CoreButtonVariant.plain,
          onPressed: openDrawer,
          child: const Icon(LucideIcons.menu),
        ),
      ],
      title: const Text('Dashboard'),
      trailing: [
        Button(
          variant: CoreButtonVariant.plain,
          onPressed: openSettings,
          child: const Icon(LucideIcons.settings),
        ),
      ],
    ),
  ],
  child: body,
)