AppBar
leading · 중앙(제목 또는 커스텀 child) · trailing 세 클러스터로 구성되는 통합 애플리케이션 바 컴포넌트
목차
AppBar는 화면 상단(또는 하단)에 놓이는 통합 애플리케이션 바입니다. leading 클러스터, 중앙 영역(title / header
/ subtitle 세 슬롯의 세로 조합, 또는 이를 통째로 대체하는 child), trailing 액션 클러스터를 가로로 배치합니다.
variant 하나로 바 모양을 고릅니다 — standard(64px 단일 행), compact(56px 단일 행),
medium(112px — leading / trailing 이 놓인 48px 상단 행 아래에 큰 제목이 하단 정렬), dense(48px 단일 행 — 데스크톱 창 · 도구 바),
prominent(56px 단일 행 — 탭 첫 화면의 큰 제목). 표면 색·높이·클러스터 간격·제목 타이포그래피·하단 헤어라인·elevated 그림자는 모두 단일
appBarStyle 슬롯을 지나며, Scaffold 의 headers / footers
슬롯에 그대로 넣어 화면 헤더 / 푸터로 쓰는 것이 일반적인 사용법입니다.
기본 사용법 (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, dense 는 labelLarge semibold,
prominent 는 headlineSmall)를 자동으로 상속하므로 Text('...') 만 넣으면 됩니다. leading / trailing 의 아이콘 버튼은
Button(variant: CoreButtonVariant.plain) 으로 합성합니다 — 투명 배경 · hover 페이드 · 포커스 링을 버튼이 스스로 그립니다.
가운데 제목 — 좌우 행동의 폭이 다를 때#
뒤로 아이콘 하나와 글자 행동 둘처럼 양쪽 클러스터의 폭이 달라도, centerTitle: true 의 제목은 클러스터 사이 공간이 아니라 바 전체의 가운데에 놓입니다. 재질(기본 · liquid glass)과 무관합니다.
AppBar(
centerTitle: true,
leading: [
Button(
variant: CoreButtonVariant.plain,
onPressed: () {},
child: const Icon(LucideIcons.arrowLeft),
),
],
title: const Text('Order details'),
trailing: [
Button(
variant: CoreButtonVariant.ghost,
size: CoreComponentSize.sm,
onPressed: () {},
child: const Text('Share'),
),
Button(
variant: CoreButtonVariant.ghost,
size: CoreComponentSize.sm,
onPressed: () {},
child: const Text('Edit'),
),
],
)
제목이 길어 가운데에 두면 넓은 쪽 클러스터에 spacing 보다 가까워지면 좁은 쪽으로 밀리고, 클러스터 사이에 아예 들어가지 않으면 그 사이 폭을 다 받아 한 줄 말줄임(…)으로 끝납니다. 어느 경우에도 클러스터를 밀어내지 않습니다.
글자 행동 — 아이콘 대신 글자로#
행동을 아이콘이 아니라 글자로 보여주는 앱도 같은 AppBar 로 만듭니다. leading 과 trailing 은
List<W> 라서 아이콘 버튼이든 글자 버튼이든 Button 을 그대로 넣으면 되고, 글자 행동은 Button(variant: CoreButtonVariant.ghost)
에 Text 를 넣어 만듭니다. 오른쪽의 보조 행동(목록으로 등)은 같은 버튼에 글자색만 한 단계 옅은 onSurfaceVariant
를 줍니다 — 자리와 누름 영역은 왼쪽과 같습니다. 글자 행동만을 위한 속성은 따로 없습니다. 버튼 크기를 적지 않으면 기본 크기(md)가 쓰입니다.
AppBar(
leading: [
Button(
variant: CoreButtonVariant.ghost,
onPressed: () {},
child: const Text('뒤로'),
),
],
trailing: [
Button(
variant: CoreButtonVariant.ghost,
buttonStyle: const CoreButtonStyle(
foregroundColor: CoreColor.token(CoreColors.onSurfaceVariant),
),
onPressed: () {},
child: const Text('목록으로'),
),
],
)
글자의 색 · 굵기 · 크기는 버튼이 상태(hover · 눌림 · 비활성)에 맞춰 정하므로 Text 에 따로 스타일을 붙이지 않습니다. 제목이 필요하면 title
을 함께 넣으면 되고, 가운데에 두려면 위의 centerTitle: true 를 씁니다.
빠른 오버라이드 (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;
}
}
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),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 화면 제목과 뒤로 가기 · 설정 같은 액션을 한 줄에 담는 헤더가 필요할 때
Scaffold의headers/footers슬롯에 넣어 화면 골격의 헤더 / 푸터를 구성할 때- 큰 제목이 별도 행에 놓이는 두 행 헤더(
medium)가 필요할 때 - 데스크톱 앱의 창 제목 바 · 도구 바처럼 32px 컨트롤 한 줄을 담는 낮은 바(
dense)가 필요할 때 - 탭의 첫 화면처럼 뒤로 가기 없이 화면 이름을 크게 보여주는 한 줄 바(
prominent)가 필요할 때
대신 다른 컴포넌트를 사용하세요:
- NavigationBar: 제목 · 액션 클러스터 대신 현재 섹션 선택과 섹션 이동이 필요할 때
- Menubar: 제목 바 대신 File / Edit / View 같은 드롭다운 메뉴 트리거 목록이 필요할 때
Props / Parameters#
W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. W?
는 선택 슬롯, List<W> 는 콘텐츠 목록입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
title |
W? |
null |
바의 주요 제목 콘텐츠 — 활성 variant 의 제목 타이포그래피를 상속 |
header | W? | null | 제목 위 보조 콘텐츠 |
subtitle |
W? |
null |
제목 아래 보조 콘텐츠 |
child |
W? |
null |
커스텀 중앙 콘텐츠 —
header
/
title
/
subtitle
열을 통째로 대체 (
title
과 동시 지정 불가)
|
leading |
List<W> |
[] |
leading(시작) 클러스터 — 보통 뒤로 가기 · 메뉴 아이콘 버튼 하나 |
trailing |
List<W> |
[] |
trailing(끝) 액션 클러스터 |
variant |
CoreAppBarVariant |
standard |
바 모양 —
standard
(64px) /
compact
(56px) /
medium
(112px 두 행) /
dense
(48px 데스크톱 바) /
prominent
(56px 한 줄 큰 제목)
|
trailingExpanded |
bool |
false |
trailing 클러스터가 남은 너비를 채우고 액션을 끝에 정렬 (단일 행 variant 에서만 의미).
centerTitle
과 함께면 가운데 맞출 자리가 없어 제목은 leading 옆에 남고 텍스트만 가운데
|
centerTitle |
bool |
false |
제목을 바 전체의 가운데에 — 좌우 클러스터의 폭이 달라도. 못 맞추면 좁은 쪽으로 밀리고, 클러스터 사이에 들어가지 않으면 한 줄 말줄임 |
showDivider |
bool |
true |
하단 헤어라인 디바이더 표시 |
elevated |
bool |
false |
elevated 드롭 섀도 표시 |
appBarStyle |
CoreAppBarStyle? |
null |
모든 chrome 이 지나는 단일 슬롯 (아래 표 참고) |
스타일 시스템 (Style System)#
CoreAppBarStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
surfaceStyle |
CoreSurfaceStyleId? |
Material override, independent of palette, typography and geometry. Null inherits the enclosing material;
defaultStyle
explicitly removes glass. Theme, variant and instance overrides use the usual merge chain. Deliberately no default: null inherits the active theme material axis; a fixed default would override that axis before the resolver sees it.
|
backgroundColor |
CoreColor? |
Bar surface colour override.
null
defers to the active variant's entry in [defaultsByVariant], then to [defaultBackgroundColor].
|
padding |
CoreEdgeInsets? |
Horizontal bar inset override — the outer inset of the cluster row (the root for standard / compact / dense / prominent, the top row for medium).
null
defers to the active variant's entry in [defaultsByVariant], then to [defaultPadding].
|
spacing |
double? |
Gap between the leading / title / actions clusters (logical px). Mirrors Flutter
Row.spacing
between the clusters.
null
defers to the active variant's entry in [defaultsByVariant], then 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 the active variant's entry in [defaultsByVariant], then 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 |
dense |
prominent |
|---|---|---|---|---|---|
height |
size64 | size56 | size112 | size48 | size56 |
titleStyle |
CoreTextStyle.token(CoreTextStyles.titleMedium, color: CoreCo… | CoreTextStyle.token(CoreTextStyles.titleMedium, color: CoreCo… | CoreTextStyle.token(CoreTextStyles.titleLarge, color: CoreCol… | CoreTextStyle.token(CoreTextStyles.labelLarge, color: CoreCol… | CoreTextStyle.token(CoreTextStyles.headlineSmall, color: Core… |
titlePadding |
CoreEdgeInsets.symmetric(horizontal: CoreSpace.space8) | CoreEdgeInsets.symmetric(horizontal: CoreSpace.space8) | CoreEdgeInsets.only(left: CoreSpace.space16, right: CoreSpace… | zero | CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16) |
padding |
— | CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16) | — | CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16) | CoreEdgeInsets.symmetric(horizontal: CoreSpace.space4) |
backgroundColor |
— | — | — | surfaceContainerLow | — |
spacing | — | — | — | space14 | — |
actionSpacing | — | — | — | space8 | — |
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 / labelLarge / headlineSmall)과 굵기를 그대로 두고 색만 바꿉니다.
위젯 슬롯 직접 주입 (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 보다 낮은 바가 필요할 때 쓰며 제목 타이포그래피는 standard
와 같습니다. 바 좌우 여백은 standard 의 8 보다 넓은 16 입니다.
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'),
)
dense#
48px 단일 행 — 데스크톱 창 제목 바 · 도구 바. 32px 컨트롤 한 줄에 위아래 8 을 더한 높이라, leading / trailing 버튼은 CoreComponentSize.sm(32px) 으로 넣습니다. 다른 변형이
surface 위에 놓이는 것과 달리 크롬 층 면(surfaceContainerLow)을 칠하고, 바 좌우 여백 16 · 클러스터 사이 14 · 액션 사이 8 ·
labelLarge semibold 제목을 씁니다. 제목 블록에는 자체 여백이 없어 클러스터 사이 14 가 제목과 이웃 사이의 유일한 거리입니다.
AppBar(
variant: CoreAppBarVariant.dense,
leading: [
Button(
variant: CoreButtonVariant.plain,
size: CoreComponentSize.sm,
onPressed: () {},
child: const Icon(LucideIcons.panelLeft),
),
],
title: const Text('Workspace'),
trailing: [
Button(
variant: CoreButtonVariant.plain,
size: CoreComponentSize.sm,
onPressed: () {},
child: const Icon(LucideIcons.settings),
),
],
)
prominent#
56px 단일 행 — 탭의 첫 화면에서 화면 이름을 크게 보여주는 바. 높이는 compact 와 같지만 제목이 headlineSmall(24 Bold) 이고 왼쪽 정렬이라, 바 안의 라벨이 아니라 화면의 머리글로 읽힙니다. 뒤로 가기 없이 쓰는 화면이라
leading 은 비우고, 오른쪽에는 글자 행동 하나를 두는 것이 기본 모양입니다.
제목은 바 가장자리에서 20 안쪽에서 시작합니다 — 바 좌우 여백 4 에 제목 여백 16 을 더한 값입니다. 글자 행동에 쓰는 ghost Button 은 자기 안쪽 여백이 16 이라, 같은 4 여백 위에서 글자가 반대쪽 가장자리에서도 20 안쪽에 끝납니다. 면은 다른 터치용 바와 같은
surface 이고 클러스터 사이 간격도 기본값을 씁니다.
AppBar(
variant: CoreAppBarVariant.prominent,
title: const Text('지도'),
trailing: [
Button(
variant: CoreButtonVariant.ghost,
buttonStyle: const CoreButtonStyle(
foregroundColor: CoreColor.token(CoreColors.onSurfaceVariant),
),
onPressed: () {},
child: const Text('목록으로'),
),
],
)
스크롤에 따라 접히는 제목은 이 변형이 하지 않습니다. 큰 제목이 두 번째 행에 놓이는 모양이 필요하면 medium 을 씁니다.
동작 스펙 (Behavior)#
바 자체는 표시용 레이아웃이며 내비게이션 · 클릭 콜백 · 비활성 상태를 소유하지 않습니다. leading / trailing 의 각 버튼·링크가 자신의 동작을 담당합니다.
레이아웃#
-
단일 행 (
standard/compact/dense/prominent):[leading] [중앙 열] [trailing]을spacing간격으로 가로 배치. 중앙 열이 남은 너비를 차지하고 자기titlePadding(기본 좌우 8,dense는 0,prominent는 16) 만큼 안으로 들어가며,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(단일 행): 제목이 바 전체의 가운데에 놓입니다 — leading · trailing 의 폭이 달라도 클러스터 사이 공간의 가운데가 아닙니다. 재질과 무관합니다.- 가운데에 두면 한쪽 클러스터와
spacing보다 가까워지는 길이면, 좁은 쪽으로 밀려 양쪽 모두spacing을 지킵니다. - 클러스터 사이에 들어가지 않을 만큼 길면 그 사이 폭을 다 받고 한 줄 말줄임으로 끝납니다 — 클러스터를 밀어내지 않습니다. 말줄임은
title에만 적용되고header·subtitle·child는 그대로입니다. trailingExpanded: true면 trailing 이 남는 폭을 가져가 가운데 맞출 자리가 없으므로, 제목은 leading 옆에 남고 텍스트만 가운데 정렬됩니다.
- 가운데에 두면 한쪽 클러스터와
centerTitle: true(medium): 큰 제목 블록이 하단 가운데에 정렬됩니다.
디바이더 · 그림자#
-
showDivider: true(기본): 바 하단에dividerColor/dividerWidth(기본outlineVariant· 1px) 헤어라인 -
Scaffold헤더 안에서는 이 헤어라인과 Scaffold 헤더 보더(CoreScaffoldStyle.headerBorderColor)가 같은 자리(헤더 하단)에 그려집니다. 둘 다 켜 두면 구조상 선이 두 겹이므로,AppBar쪽을showDivider: false로 끄고 Scaffold 헤더 보더를 경계선의 단일 출처로 둡니다 — 아래 사용 가이드라인 참조. -
elevated: true:elevatedShadow(기본CoreShadow.sm) 드롭 섀도. 표면 채움이 불투명한 variant 면(surface,dense는surfaceContainerLow)이라 그림자가 바 아래로 비치지 않습니다.
사용 가이드라인 (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'),
showDivider: false,
),
],
child: body,
)
Scaffold 가 헤더 섹션을 본문 위에 쌓고, 플로팅 헤더 · 로딩 인디케이터와의 겹침을 처리합니다. 헤더 하단 경계선은 Scaffold 헤더 보더가 그리므로 AppBar 의 헤어라인은 showDivider: false 로 끕니다 — 두 선은 같은 자리에 그려집니다.
❌ Don't#
title 과 child 를 동시에 주기
// ❌ assert 실패 — 두 슬롯은 서로 배타적이다
AppBar(
title: const Text('Dashboard'),
child: searchField,
)
child 는 header / title / subtitle 열을 통째로 대체하는 슬롯입니다. 검색 필드처럼 자유 형식 중앙 콘텐츠가 필요하면 child 만 주고, 제목이 필요하면 title 계열만 씁니다.
Scaffold 헤더 안에서 AppBar 디바이더를 켜 둔 채 쓰기
// ❌ Scaffold 헤더 보더와 AppBar 헤어라인이 같은 자리에 두 번 그려진다
Scaffold(
headers: [
AppBar(title: const Text('Dashboard')), // showDivider 기본값 true
],
child: body,
)
Scaffold 헤더 보더와 AppBar 하단 디바이더는 둘 다 헤더 하단에 그려집니다. 기본값끼리는 같은 outlineVariant 1px 라 한 줄처럼 보이지만 구조상 이중이고, 한쪽 색(dividerColor 또는 headerBorderColor)만 바꾸면 두 선이 어긋나 드러납니다. AppBar(showDivider: false) 로 끄고 경계선은 CoreScaffoldStyle.headerBorderColor / headerBorderWidth 한 곳에서 정합니다.
높이 · 색을 바깥 래퍼로 덮어쓰기
// ❌ 바가 자기 chrome 을 가진다 — 래퍼로 덮으면 variant · 테마 오버라이드와 어긋난다
SizedBox(
height: 80,
child: AppBar(title: const Text('Dashboard')),
)
높이 · 표면 색 · 간격은 전부 appBarStyle (또는 프로젝트의 CoreAppBarTheme) 로 흐릅니다. withStyle(const CoreAppBarStyle(height: CoreSize.size80)) 처럼 슬롯으로 주면 Web · Flutter 가 같이 움직입니다.
접근성 (Accessibility)#
역할#
바 자체는 랜드마크 역할을 자동으로 붙이지 않습니다. leading / trailing 의
버튼·링크와 제목 콘텐츠는 각자의 의미를 유지합니다. 페이지 헤더 의미는 호출부가
Flutter 시맨틱 래퍼 또는 Web attributes: {'role': 'banner'} 로 제공합니다.
스크린 리더#
-
Flutter: 바 자체는 시맨틱 래퍼를 추가하지 않습니다 — leading / trailing 의
Button과title의Text가 각자의 시맨틱을 그대로 노출합니다. 읽기 순서는 leading → 중앙 열 → trailing. -
Web: 루트가
<div>이며 랜드마크 role 을 자동으로 붙이지 않습니다. 페이지 헤더로 쓰면attributes: {'role': 'banner'}를 넘겨 랜드마크로 노출하세요.
키보드#
바 자체는 포커스를 받지 않습니다. leading / trailing 의 Button 이 DOM · 위젯 순서대로 Tab 포커스를 받고,
Enter / Space 로 활성화됩니다 — 포커스 링은 각 버튼이 그립니다.
포커스 관리#
진입·이탈 시 포커스를 옮기거나 가두지 않습니다. 자식 컨트롤의 포커스와 링을 그대로 유지합니다.
알려진 제약#
아이콘만 넣은 액션의 이름은 AppBar 가 대신 만들지 않습니다. 호출부에서 버튼의 접근 가능한 이름을 채우고, 제목을 heading 으로 읽어야 하면 그 의미도 슬롯에 실으세요.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | AppBar | AppBar |
| 렌더 루트 | DecoratedBox + SizedBox(height:) |
<div> (flex row · w-full) |
| 가운데 제목 (단일 행) | CustomMultiChildLayout — 바 가운데, 못 맞추면 좁은 쪽으로 spacing 을 지키며 밀림 |
세 칸 CSS grid(
minmax(max-content, 1fr) minmax(0, max-content) minmax(max-content, 1fr)
) — 같은 자리, 측정 스크립트 없음
|
| 하단 디바이더 | 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,
)