AppBar#
AppBar는 화면 상단(또는 하단)에 놓이는 통합 애플리케이션 바입니다. leading 클러스터, 중앙 영역(title / header
/ subtitle 세 슬롯의 세로 조합, 또는 이를 통째로 대체하는 child), trailing 액션 클러스터를 가로로 배치합니다.
variant 하나로 바 모양을 고릅니다 — standard(64px 단일 행), compact(56px 단일 행),
medium(112px — leading / trailing 이 놓인 48px 상단 행 아래에 큰 제목이 하단 정렬). 표면 색·높이·클러스터 간격·제목 타이포그래피·하단 헤어라인·elevated 그림자는 모두 단일
appBarStyle 슬롯을 지나며, Scaffold 의 headers / footers
슬롯에 그대로 넣어 화면 헤더 / 푸터로 쓰는 것이 일반적인 사용법입니다.
Live Preview#
class AppBarDefaultExample extends StatelessComponent {
const AppBarDefaultExample({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),
),
],
);
}
}
class AppBarDefaultExample extends StatelessWidget {
const AppBarDefaultExample({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),
),
],
);
}
}
class AppBarCompactExample extends StatelessComponent {
const AppBarCompactExample({super.key});
@override
Component build(BuildContext context) {
return AppBar(
variant: CoreAppBarVariant.compact,
leading: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.arrowLeft),
),
],
title: const Text('Notifications'),
trailing: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.check),
),
],
);
}
}
class AppBarCompactExample extends StatelessWidget {
const AppBarCompactExample({super.key});
@override
Widget build(BuildContext context) {
return AppBar(
variant: CoreAppBarVariant.compact,
leading: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.arrowLeft),
),
],
title: const Text('Notifications'),
trailing: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.check),
),
],
);
}
}
class AppBarMediumExample extends StatelessComponent {
const AppBarMediumExample({super.key});
@override
Component build(BuildContext context) {
return AppBar(
variant: CoreAppBarVariant.medium,
leading: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.arrowLeft),
),
],
title: const Text('Inbox'),
trailing: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.ellipsisVertical),
),
],
);
}
}
class AppBarMediumExample extends StatelessWidget {
const AppBarMediumExample({super.key});
@override
Widget build(BuildContext context) {
return AppBar(
variant: CoreAppBarVariant.medium,
leading: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.arrowLeft),
),
],
title: const Text('Inbox'),
trailing: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.ellipsisVertical),
),
],
);
}
}
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;
}
}
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;
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 화면 제목과 뒤로 가기 · 설정 같은 액션을 한 줄에 담는 헤더가 필요할 때
Scaffold의headers/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 / compact 는 titleMedium,
medium 은 titleLarge)를 자동으로 상속하므로 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)#
| 필드 | standard | compact | medium |
|---|---|---|---|
height | size64 | size56 | size112 |
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는 영향이 없습니다. -
중앙 열:
header→title→subtitle순의 세로 열.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 만 넣으면 클릭도 포커스도 되지 않습니다.
Scaffold 의 headers / footers 슬롯에 넣기
Scaffold(
headers: [
AppBar(title: const Text('Dashboard')),
],
child: body,
)
Scaffold 가 헤더 섹션을 본문 위에 쌓고, 플로팅 헤더 · 로딩 인디케이터와의 겹침을 처리합니다.
❌ Don't#
title 과 child 를 동시에 주기
// ❌ assert 실패 — 두 슬롯은 서로 배타적이다
AppBar(
title: const Text('Dashboard'),
child: searchField,
)
child 는 header / 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 의
Button과title의Text가 각자의 시맨틱을 그대로 노출합니다. 읽기 순서는 leading → 중앙 열 → trailing. -
Web: 루트가
<div>이며 랜드마크 role 을 자동으로 붙이지 않습니다. 페이지 헤더로 쓰면attributes: {'role': 'banner'}를 넘겨 랜드마크로 노출하세요.
키보드 인터랙션#
바 자체는 포커스를 받지 않습니다. leading / trailing 의 Button 이 DOM · 위젯 순서대로 Tab 포커스를 받고,
Enter / Space 로 활성화됩니다 — 포커스 링은 각 버튼이 그립니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | AppBar | AppBar |
| 렌더 루트 | 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
통과
|
관련 컴포넌트 (Related Components)#
-
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,
)