NavigationBar#
NavigationBar는 주요 섹션 간 이동을 위한 통합 네비게이션 컨테이너입니다. mode 한 가지로 가로 바(bar), 세로 레일(rail), 확장 사이드바(sidebar) 세 가지 레이아웃을 모두 표현하며, 아이템은 단일
CoreNavigationBarItem 데이터 모델로 구성합니다.
Live Preview#
class NavigationBarDefaultExample extends StatefulComponent {
const NavigationBarDefaultExample({super.key});
@override
State<NavigationBarDefaultExample> createState() =>
_NavigationBarDefaultExampleState();
}
class _NavigationBarDefaultExampleState
extends State<NavigationBarDefaultExample> {
int _selectedIndex = 0;
@override
Component build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.bar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.all,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Alerts',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
);
}
}
class NavigationBarDefaultExample extends StatefulWidget {
const NavigationBarDefaultExample({super.key});
@override
State<NavigationBarDefaultExample> createState() =>
_NavigationBarDefaultExampleState();
}
class _NavigationBarDefaultExampleState
extends State<NavigationBarDefaultExample> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.bar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.all,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Alerts',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
);
}
}
class NavigationBarRailExample extends StatefulComponent {
const NavigationBarRailExample({super.key});
@override
State<NavigationBarRailExample> createState() =>
_NavigationBarRailExampleState();
}
class _NavigationBarRailExampleState extends State<NavigationBarRailExample> {
int _selectedIndex = 0;
@override
Component build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.rail,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.selected,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Alerts',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
);
}
}
class NavigationBarRailExample extends StatefulWidget {
const NavigationBarRailExample({super.key});
@override
State<NavigationBarRailExample> createState() =>
_NavigationBarRailExampleState();
}
class _NavigationBarRailExampleState extends State<NavigationBarRailExample> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.rail,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.selected,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Alerts',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
);
}
}
class NavigationBarSidebarExample extends StatefulComponent {
const NavigationBarSidebarExample({super.key});
@override
State<NavigationBarSidebarExample> createState() =>
_NavigationBarSidebarExampleState();
}
class _NavigationBarSidebarExampleState
extends State<NavigationBarSidebarExample> {
int _selectedIndex = 0;
@override
Component build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.sidebar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
items: const [
CoreNavigationBarItem.label('Main'),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Dashboard',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Explore',
),
CoreNavigationBarItem.divider(),
CoreNavigationBarItem.label('Account'),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Notifications',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.settings),
label: 'Settings',
),
],
);
}
}
class NavigationBarSidebarExample extends StatefulWidget {
const NavigationBarSidebarExample({super.key});
@override
State<NavigationBarSidebarExample> createState() =>
_NavigationBarSidebarExampleState();
}
class _NavigationBarSidebarExampleState
extends State<NavigationBarSidebarExample> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.sidebar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
items: const [
CoreNavigationBarItem.label('Main'),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Dashboard',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Explore',
),
CoreNavigationBarItem.divider(),
CoreNavigationBarItem.label('Account'),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Notifications',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.settings),
label: 'Settings',
),
],
);
}
}
class NavigationBarChainExample extends StatefulComponent {
const NavigationBarChainExample({super.key});
@override
State<NavigationBarChainExample> createState() => _NavigationBarChainExampleState();
}
class _NavigationBarChainExampleState extends State<NavigationBarChainExample> {
int _selectedIndex = 0;
@override
Component build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.bar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.all,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Alerts',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
)
.withStyle(
const CoreNavigationBarStyle(
itemSpacing: CoreSpace.space12,
itemPadding: CoreEdgeInsets.all(CoreSpace.space12),
itemRadius: CoreBorderRadius.all(CoreRadius.radius16),
),
)
.surfaceContainer;
}
}
class NavigationBarChainExample extends StatefulWidget {
const NavigationBarChainExample({super.key});
@override
State<NavigationBarChainExample> createState() => _NavigationBarChainExampleState();
}
class _NavigationBarChainExampleState extends State<NavigationBarChainExample> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.bar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.all,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Alerts',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
)
.withStyle(
const CoreNavigationBarStyle(
itemSpacing: CoreSpace.space12,
itemPadding: CoreEdgeInsets.all(CoreSpace.space12),
itemRadius: CoreBorderRadius.all(CoreRadius.radius16),
),
)
.surfaceContainer;
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 모바일 앱 하단의 주요 3~5개 섹션 이동(
bar모드) - 데스크톱/태블릿 측면의 컴팩트 네비게이션(
rail모드) - 라벨이 강조된 풀-너비 사이드바 네비게이션(
sidebar모드)
대신 다른 컴포넌트를 사용하세요:
NavigationMenu: 드롭다운 패널이 있는 가로 메가 메뉴가 필요할 때Tabs: 동일 화면 내에서 콘텐츠 탭을 전환할 때
기본 사용법 (Basic Usage)#
NavigationBar(
mode: CoreNavigationBarMode.bar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.all,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
)
NavigationBar(
mode: CoreNavigationBarMode.bar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.all,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
)
빠른 오버라이드 (Chain)#
이미 만든 NavigationBar 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.surfaceContainer처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(surfaceContainer ==
CoreColors.surfaceContainer) 어느 컴포넌트에서 써도 뜻이 갈리지 않으며, 위 예시처럼 withStyle 뒤에 이어붙일 수도 있습니다.
class NavigationBarChainExample extends StatefulWidget {
const NavigationBarChainExample({super.key});
@override
State<NavigationBarChainExample> createState() => _NavigationBarChainExampleState();
}
class _NavigationBarChainExampleState extends State<NavigationBarChainExample> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.bar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.all,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Alerts',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
)
.withStyle(
const CoreNavigationBarStyle(
itemSpacing: CoreSpace.space12,
itemPadding: CoreEdgeInsets.all(CoreSpace.space12),
itemRadius: CoreBorderRadius.all(CoreRadius.radius16),
),
)
.surfaceContainer;
}
}
class NavigationBarChainExample extends StatefulComponent {
const NavigationBarChainExample({super.key});
@override
State<NavigationBarChainExample> createState() => _NavigationBarChainExampleState();
}
class _NavigationBarChainExampleState extends State<NavigationBarChainExample> {
int _selectedIndex = 0;
@override
Component build(BuildContext context) {
return NavigationBar(
mode: CoreNavigationBarMode.bar,
selectedIndex: _selectedIndex,
onSelected: (index) => setState(() => _selectedIndex = index),
labelMode: CoreNavigationLabelMode.all,
items: const [
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.house),
label: 'Home',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.search),
label: 'Search',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.bell),
label: 'Alerts',
),
CoreNavigationBarItem.destination(
icon: Icon(LucideIcons.user),
label: 'Profile',
),
],
)
.withStyle(
const CoreNavigationBarStyle(
itemSpacing: CoreSpace.space12,
itemPadding: CoreEdgeInsets.all(CoreSpace.space12),
itemRadius: CoreBorderRadius.all(CoreRadius.radius16),
),
)
.surfaceContainer;
}
}
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
items |
List<CoreNavigationBarItem> |
필수 | 네비게이션 엔트리 목록 |
selectedIndex |
int? |
null |
현재 선택된 destination 인덱스 (destination만 카운트) |
onSelected |
void Function(int)? |
null |
destination 선택 핸들러 |
mode |
CoreNavigationBarMode |
bar |
레이아웃 모드 (bar / rail / sidebar) |
variant |
CoreNavigationBarVariant |
standard |
시각 변형 |
labelMode |
CoreNavigationLabelMode? |
모드별 기본값 | 라벨 표시 시점 (all / selected / none) |
labelPosition |
CoreNavigationLabelPosition? |
모드별 기본값 | 아이콘 대비 라벨 위치 (bottom / top / start / end) |
alignment |
CoreNavigationAlignment |
center |
메인 축 정렬 |
overflow |
CoreNavigationOverflow |
ellipsis |
라벨 오버플로 처리 |
navigationBarStyle |
CoreNavigationBarStyle? |
null |
크롬 / 치수 스타일 오버라이드 |
CoreNavigationBarItem#
| 팩토리 | 설명 |
|---|---|
CoreNavigationBarItem.destination({icon, label, tooltip, enabled}) |
선택 가능한 네비게이션 목적지 |
CoreNavigationBarItem.label(String) | 비선택 섹션 헤더 라벨 |
CoreNavigationBarItem.divider() | 구분선 |
CoreNavigationBarItem.gap(double) | 빈 간격 |
스타일 시스템 (Style System)#
CoreNavigationBarStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
Background colour of the navigation container surface. |
padding |
CoreEdgeInsets? |
Outer padding of the navigation container. |
barRadius |
CoreBorderRadius? |
The bar's own corners. Distinct from the item radius: this rounds the bar, which is what makes a detached floating bar read as one object rather than a strip with rounded buttons in it.
null
leaves the bar square, which is what a bar docked to an edge wants.
|
barMargin |
CoreEdgeInsets? |
Space outside the bar. What detaches the bar from the edge it would otherwise sit flush against. Padding cannot express this — padding moves the items and leaves the bar's own box where it was. |
barBorderColor |
CoreColor? |
Colour of the bar's content-facing hairline. Drawn on the edge that faces the navigated content — the top edge of a horizontal bar, the trailing (logical end) edge of a rail / sidebar. |
barBorderWidth |
double? |
Thickness of the bar's content-facing hairline (logical px). |
itemSpacing |
double? |
Gap between adjacent navigation items (logical px). The baseline lives in [defaultsByMode], not in a
defaultItemSpacing
constant. There used to be one, and it was a single
space8
for all three modes — which
bar
does not want: its items sit flush at
0
. A scalar default cannot state that, so it cannot come back; read
defaultsByMode[mode].spacing
instead.
|
itemPadding |
CoreEdgeInsets? |
Inner padding of a navigation item. |
itemRadius |
CoreBorderRadius? |
Corner radius of a navigation item. |
itemMinHeight |
double? |
Minimum height of a navigation item (logical px). |
labelSpacing |
double? |
Native flex
spacing
between an item's icon and its label — passed directly to
Row.spacing
/
Column.spacing
(Flutter) and the inner flex
gap
(Web). Not wrapped in
Gap
(logical px).
|
iconStyle |
CoreIconStyle? |
Navigation item icon style override. size defers to [defaultIconStyle]. |
selectedBackgroundColor |
CoreColor? |
Background colour of the selected item. |
selectedLabelTextStyle |
CoreTextStyle? |
Label text style override for a selected item. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
selectedForegroundColor
field removed). Defaults to [defaultSelectedLabelTextStyle].
|
hoverBackgroundColor |
CoreColor? |
Background colour applied to an item on hover. Defaults to [defaultHoverBackgroundColor] — transparent, i.e. no swap. Set it to paint a fill on hover INSTEAD of [stateLayer]; the two do not stack, because two mechanisms saying the same state is how this component's hover drifted in the first place. |
stateLayer |
CoreStateLayer? |
Ramp laid over the bar while a destination is hovered, focused or pressed. Defaults to [defaultStateLayer]. |
stateLayerColor |
CoreColor? |
Ink [stateLayer] lays over the bar. Defaults to [defaultStateLayerColor]. |
labelTextStyle |
CoreTextStyle? |
Label text style override for an unselected item. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
foregroundColor
field removed). Defaults to [defaultLabelTextStyle].
|
disabledLabelTextStyle |
CoreTextStyle? |
Label text style override for a disabled item. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
disabledForegroundColor
field removed). Defaults to [defaultDisabledLabelTextStyle].
|
sectionLabelTextStyle |
CoreTextStyle? |
Text style override for a non-selectable section header. The semibold weight and
onSurfaceVariant
colour live in the [defaultSectionLabelTextStyle] overlay; this slot tweaks them per instance. Defaults to [defaultSectionLabelTextStyle].
|
dividerColor |
CoreColor? |
Colour of a navigation divider line. |
dividerThickness |
double? |
Thickness of a navigation divider line (logical px). |
duration |
Duration? |
Colour-transition duration for item hover / selection. |
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed destination
Clickable
(press scale / focus ring / cursor / keyboard activation). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.
|
CoreNavigationBarStyle 변형별 기본값 (CoreNavigationBarVariantStyle)#
| 필드 | standard |
|---|---|
backgroundColor | surfaceBright |
모드 (Modes)#
Bar#
가로 바 레이아웃입니다. 모바일 앱 하단 네비게이션에 적합합니다.
NavigationBar(mode: CoreNavigationBarMode.bar, items: [...])
Rail#
세로 컴팩트 레일 레이아웃입니다. 측면 네비게이션에 적합합니다.
NavigationBar(mode: CoreNavigationBarMode.rail, items: [...])
Sidebar#
확장 세로 사이드바 레이아웃입니다. 섹션 라벨과 구분선으로 계층 구조를 표현합니다.
NavigationBar(
mode: CoreNavigationBarMode.sidebar,
items: const [
CoreNavigationBarItem.label('Main'),
CoreNavigationBarItem.destination(label: 'Dashboard'),
CoreNavigationBarItem.divider(),
CoreNavigationBarItem.label('Account'),
CoreNavigationBarItem.destination(label: 'Settings'),
],
)
동작 스펙 (Behavior)#
인터랙션#
- 선택: destination 활성화 시
onSelected(index)호출 (destination만 인덱싱) - 호버: destination 호버 시 배경색 약한 강조
- 키보드: 포커스된 destination에서
Enter/Space로 활성화
애니메이션#
- 활성/호버 전환: 배경·전경색 변경 150ms ease-in-out
사용 가이드라인 (Usage Guidelines)#
✅ Do#
mode 에 맞는 기본 라벨 동작을 그대로 사용
NavigationBar(
mode: CoreNavigationBarMode.rail,
selectedIndex: index,
onSelected: (i) => setState(() => index = i),
items: destinations,
)
labelMode/labelPosition 을 생략하면 모드별 기본값(bar → none/bottom, rail → selected/bottom, sidebar → all/end)이 적용됩니다. 세 모드 모두 라벨 표시 방식을 매번 직접 지정하지 않아도 되는 이유입니다.
❌ Don't#
selectedIndex 를 items 리스트의 원시 인덱스와 혼동하지 않기
// ❌ label/divider 항목까지 포함한 리스트 인덱스를 그대로 사용
NavigationBar(
items: const [
CoreNavigationBarItem.label('Main'), // 선택 불가 — 인덱싱 제외
CoreNavigationBarItem.destination(label: 'Home'), // 실제 selectedIndex 0
CoreNavigationBarItem.divider(), // 선택 불가 — 인덱싱 제외
CoreNavigationBarItem.destination(label: 'Settings'), // 실제 selectedIndex 1
],
selectedIndex: 1, // ❌ 'Home'을 고르려던 의도였다면 어긋남
)
selectedIndex/onSelected 의 인덱스는 선택 가능한 destination 항목만 카운트합니다 — label/divider/gap 은 인덱싱에서 건너뜁니다. items 리스트의 원시 인덱스를 그대로 넘기면 어긋납니다.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
Tab | 다음 네비게이션 항목으로 포커스 |
Enter / Space | 포커스된 항목 활성화 |
스크린 리더#
-
Flutter:
Semantics(selected: isSelected, button: true, label: itemLabel)자동 적용 -
Web:
<nav aria-label="Navigation">+ 각 항목role="button"+ 선택 시aria-current="page"적용
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | NavigationBar | NavigationBar |
| 렌더 루트 | ColoredBox + SingleChildScrollView |
<nav> (flex) |
| 라벨 오버플로 | TextOverflow | CSS text-overflow |
관련 컴포넌트 (Related Components)#
- NavigationMenu: 드롭다운 패널이 있는 가로 메가 메뉴
- Tabs: 화면 내 콘텐츠 섹션 전환에 사용