NavigationBarItem
내비게이션 항목 — 바 밖에서 쓰는 사이드바 한 줄
목차
NavigationBarItem 은 내비게이션 목적지 하나입니다 — 아이콘, 이름, 개수 배지, 그리고 지금 있는
곳인지를 말하는 면. NavigationBar 가 목적지마다 합성하는 바로 그 위젯이고, 공개되어 있어서 바
밖에서도 씁니다 — Drawer 안의 사이드바, 앱이 직접 조립하는 레일, 설정 화면의 메뉴 열. 손으로
놓은 줄과 바가 놓은 줄은 같은 위젯이라 똑같이 보이고 똑같이 동작합니다.
기본 사용법 (Basic Usage)#
NavigationBarItem(
icon: const Icon(LucideIcons.inbox),
label: 'Inbox',
badge: const Badge(
form: CoreBadgeForm.count,
tone: CoreBadgeTone.info,
variant: CoreBadgeVariant.filled,
size: CoreComponentSize.xs,
child: Text('3'),
),
selected: page == 0,
onPressed: () => setState(() => page = 0),
)
NavigationBarItem(
icon: const Icon(LucideIcons.inbox),
label: 'Inbox',
badge: const Badge(
form: CoreBadgeForm.count,
tone: CoreBadgeTone.info,
variant: CoreBadgeVariant.filled,
size: CoreComponentSize.xs,
child: Text('3'),
),
selected: page == 0,
onPressed: () => setState(() => page = 0),
)
선택은 항목을 놓는 화면이 가집니다 — 항목은 selected 를 그리고 onPressed 를 부를 뿐입니다.
줄은 받은 폭을 채우고, 높이는 내용(그리고 itemMinHeight)만큼입니다. 여러 줄은 Column
으로
쌓습니다 — 사이드바 밀도의 줄 사이 간격은 CoreSpace.space2 입니다.
Live Preview#
코드 보기
class NavigationBarItemDefaultExample extends StatefulComponent {
const NavigationBarItemDefaultExample({super.key});
@override
State<NavigationBarItemDefaultExample> createState() => _NavigationBarItemDefaultExampleState();
}
class _NavigationBarItemDefaultExampleState extends State<NavigationBarItemDefaultExample> {
int _page = 0;
@override
Component build(BuildContext context) {
return div(
[
NavigationBarItem(
icon: const Icon(LucideIcons.inbox),
label: 'Inbox',
badge: const Badge(
form: CoreBadgeForm.count,
tone: CoreBadgeTone.info,
variant: CoreBadgeVariant.filled,
size: CoreComponentSize.xs,
child: Text('3'),
),
selected: _page == 0,
onPressed: () => setState(() => _page = 0),
),
NavigationBarItem(
icon: const Icon(LucideIcons.bell),
label: 'Notifications',
selected: _page == 1,
onPressed: () => setState(() => _page = 1),
),
NavigationBarItem(
icon: const Icon(LucideIcons.settings),
label: 'Settings',
selected: _page == 2,
onPressed: () => setState(() => _page = 2),
),
const NavigationBarItem(
icon: Icon(LucideIcons.lock),
label: 'Admin',
enabled: false,
),
],
classes:
'flex flex-col w-full max-w-${CoreLayout.scale.layout240} '
'gap-${CoreSpace.scale.space2}',
);
}
}
class NavigationBarItemDefaultExample extends StatefulWidget {
const NavigationBarItemDefaultExample({super.key});
@override
State<NavigationBarItemDefaultExample> createState() => _NavigationBarItemDefaultExampleState();
}
class _NavigationBarItemDefaultExampleState extends State<NavigationBarItemDefaultExample> {
int _page = 0;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: CoreLayout.layout240),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: CoreSpace.space2,
children: [
NavigationBarItem(
icon: const Icon(LucideIcons.inbox),
label: 'Inbox',
badge: const Badge(
form: CoreBadgeForm.count,
tone: CoreBadgeTone.info,
variant: CoreBadgeVariant.filled,
size: CoreComponentSize.xs,
child: Text('3'),
),
selected: _page == 0,
onPressed: () => setState(() => _page = 0),
),
NavigationBarItem(
icon: const Icon(LucideIcons.bell),
label: 'Notifications',
selected: _page == 1,
onPressed: () => setState(() => _page = 1),
),
NavigationBarItem(
icon: const Icon(LucideIcons.settings),
label: 'Settings',
selected: _page == 2,
onPressed: () => setState(() => _page = 2),
),
const NavigationBarItem(
icon: Icon(LucideIcons.lock),
label: 'Admin',
enabled: false,
),
],
),
);
}
}
구조 (Anatomy)#
사이드바 밀도(기본)에서는 한 줄입니다 — 아이콘과 이름이 시작 쪽에, 배지가 줄 끝에 붙습니다. 바 ·
레일 밀도(mode: bar / rail)에서는 이름이 아이콘 아래로 쌓이고 배지가 아이콘의 위쪽 끝 모서리에
걸립니다.
| 부분 | 역할 | 슬롯 |
|---|---|---|
| 아이콘 | 목적지의 그림. 없으면 이름만 그립니다 | icon |
| 이름 | 목적지의 이름이자 접근성 이름. 숨겨도(labelMode) 이름과 툴팁으로 남습니다 |
label |
| 배지 | 개수 같은 표시. 이름 뒤에 이어 읽힙니다 | badge |
| 면 | 선택 · 호버 · 포커스 · 눌림을 칠하는 칸 | navigationBarStyle |
Figma: ❖ NavigationBarItem
Figma 속성 대응#
Figma 세트의 축과 속성이 코드의 어느 값인지입니다. 이름이 다른 곳은 그대로 적어 둡니다 — Figma 쪽 이름은 디자이너가 정한 이름이라 코드가 바꾸지 않습니다.
| Figma | 코드 | 비고 |
|---|---|---|
Label (텍스트) | label | |
Icon (인스턴스 교체) | icon | |
Show icon | icon 이 있는가 | 끄면 이름만 있는 목적지 |
Show count | badge | 개수를 끄고 켜는 속성. 코드는 배지 위젯을 받습니다 |
Show mark |
selectionMark: dot |
이름이 아이콘 아래 · 위로 쌓일 때만 그립니다 |
Selected | selected | |
State=disabled | enabled: false | |
State=hover · focus | — | 포인터와 키보드가 만드는 상태라 속성이 아닙니다 |
Label=end · bottom |
labelPosition |
end 는 사이드바 줄(mode: sidebar), bottom 은 바 · 레일 |
Label=none | labelMode: none | 이름은 숨겨도 접근성 이름으로 남습니다 |
Indicator=fill · color |
indicator |
fill
은 타일(이름을 숨기면 아이콘 뒤 알약),
color
는 면 없이 색만.
color
는 바의 쌓인 목적지라
Label=bottom
에만 있습니다
|
Show dot |
showDot |
개수 없이 새 소식을 알리는 점. Show count 가 켜지면 개수가 이깁니다 |
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
Drawer안에 사이드바를 그릴 때 — 항목을 손으로 그리지 않고 이 위젯을 놓습니다- 앱이 사이드바의 구조(구획 · 접기 · 계정 칸)를 직접 조립하면서 항목은 디자인 시스템의 것을 쓸 때
- 설정 화면처럼 목록 하나가 "지금 보는 곳" 을 표시해야 할 때
대신 다른 컴포넌트를 사용하세요:
- NavigationBar: 항목 목록을 데이터로 넘기면 바 · 레일 · 사이드바를 통째로 그려 줍니다 — 구획 이름 · 구분선 · 아래 칸 · 글래스 선택까지 함께
- MenuItem: 메뉴 패널 안의 행동 한 줄 — 장소가 아니라 명령입니다
Props / Parameters#
W 는 Flutter의 Widget / Web의 Jaspr Component입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
icon |
W? |
null |
앞의 아이콘. 없으면 이름만 그립니다 |
label |
String? |
null |
목적지 이름. 숨겨도 접근성 이름과 툴팁으로 남습니다 |
badge |
W? |
null |
개수 같은 표시. 사이드바 밀도에서는 줄 끝, 바 · 레일 밀도에서는 아이콘 모서리 |
tooltip |
String? |
null |
이름이 숨겨졌을 때 호버로 보여 줄 글. 없으면 이름 |
selected |
bool |
false |
지금 있는 곳인가. 비활성 항목은 선택되지 않습니다 |
enabled |
bool? |
null |
활성 여부. null 이면 onPressed 가 있을 때만 활성 |
onPressed |
void Function()? |
null |
클릭 · 탭 · 포커스 상태의 Enter / Space |
mode |
CoreNavigationBarMode |
CoreNavigationBarItemContract.defaultMode(sidebar) |
그리는 밀도 — 사이드바 줄, 또는 바 · 레일의 쌓인 목적지 |
variant |
CoreNavigationBarVariant |
standard |
바의 시각 변형 |
labelMode |
CoreNavigationLabelMode? |
모드별 기본값 | 이름을 보이는 때 (all / selected / none) |
labelPosition |
CoreNavigationLabelPosition? |
모드별 기본값 | 아이콘에 대한 이름의 자리 (end / start / bottom / top) |
overflow |
CoreNavigationOverflow |
ellipsis |
남은 폭보다 긴 이름의 처리 |
selectionMark |
CoreNavigationSelectionMark |
none |
면과 이름 말고 선택을 알리는 점. 이름이 쌓일 때만 그립니다 |
indicator |
CoreNavigationSelectionIndicator? |
모드별 기본값 |
선택 표시 방식 —
fill
(타일, 이름을 숨긴 쌓인 목적지는 아이콘 뒤 알약) 또는
color
(면 없이 아이콘 · 이름 색). 바는
color
, 레일 · 사이드바는
fill
|
showDot |
bool |
false |
아이콘 끝 위 모서리의 알림 점. badge 가 있으면 badge 가 이깁니다 |
navigationBarStyle |
CoreNavigationBarStyle? |
null |
바의 스타일 — 항목은 그 항목 필드를 그립니다 |
모드별 기본값은 CoreNavigationBarMode.defaultLabelMode · defaultLabelPosition · defaultIndicator
가 정합니다 — 바가
목적지를 그릴 때와 같은 답입니다.
스타일 시스템 (Style System)#
navigationBarStyle#
항목의 크롬은 바의 것입니다. navigationBarStyle 은 CoreNavigationBarStyle 이고, 항목은 그 중
항목 필드(itemPadding · itemRadius · itemMinHeight
· labelSpacing · 상태별 면과 이름 · 아이콘 ·
배지 · 상태 레이어)를 mode 의 밀도로 읽습니다. 필드 표는 NavigationBar
문서의 스타일 시스템 절에 있습니다.
-
시맨틱 / behaviour: 위젯 파라미터로 직접 (
selected·enabled·mode·labelMode등) - chrome / dimensional:
CoreNavigationBarStyle한 곳으로
Resolve chain
CoreNavigationBarStyle.defaultX · defaultsByMode[mode] // 기본값과 밀도
→ CoreNavigationBarTheme.style // 프로젝트 공통
→ CoreNavigationBarTheme.variantStyles[variant]
→ widget.navigationBarStyle // 인스턴스별
내비게이션 바를 테마로 바꾼 프로젝트는 단독 항목도 함께 바뀝니다.
빠른 오버라이드 (Chain)
이미 만든 NavigationBarItem 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle
체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만,
이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class NavigationBarItemChainExample extends StatelessWidget {
const NavigationBarItemChainExample({super.key});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: CoreLayout.layout240),
child:
NavigationBarItem(
icon: const Icon(LucideIcons.inbox),
label: 'Inbox',
selected: true,
onPressed: () {},
).withStyle(
const CoreNavigationBarStyle(
selectedBackgroundColor: CoreColor.token(CoreColors.primaryContainer),
itemRadius: CoreBorderRadius.all(CoreRadiusRole.panel),
),
),
);
}
}
class NavigationBarItemChainExample extends StatelessComponent {
const NavigationBarItemChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
NavigationBarItem(
icon: const Icon(LucideIcons.inbox),
label: 'Inbox',
selected: true,
onPressed: () {},
).withStyle(
const CoreNavigationBarStyle(
selectedBackgroundColor: CoreColor.token(
CoreColors.primaryContainer,
),
itemRadius: CoreBorderRadius.all(CoreRadiusRole.panel),
),
),
],
classes: 'w-full max-w-${CoreLayout.scale.layout240}',
);
}
}
동작 스펙 (Behavior)#
인터랙션#
-
활성:
onPressed가 있으면 활성입니다(enabled ?? onPressed != null). 클릭 · 탭, 그리고 포커스를 가진 상태의Enter/Space가onPressed를 부릅니다 - 비활성: 면은 쉬는 면, 이름과 아이콘은 비활성 색입니다. 선택돼 있어도 선택으로 그리지 않고 선택으로 읽히지도 않습니다
- 상태 면: 호버 · 포커스 · 눌림이 각자의 면을 칠합니다(상태 레이어). 선택된 항목도 선택 표시를 유지한 채 그 위에 같은 상태 레이어를 받습니다
-
선택 면 (
fill): 이름을 옆에 둔 줄은primaryContainer면에 아이콘 · 이름이onPrimaryContainer입니다. 이름을 아래에 쌓거나 숨긴 목적지는 타일 없이 아이콘 둘레 알약만primaryContainer이고 아이콘은onPrimaryContainer, 이름은onSurface입니다 — 같은 쌍이라 레일을 접었다 펼쳐도 같은 선택으로 읽힙니다 - 포커스: 모든
indicator에서onSurface8 % 상태 레이어와 포커스 링을 그립니다
배치#
-
사이드바 밀도: 줄이 받은 폭을 채우고, 아이콘과 이름이 시작 쪽에 붙고, 배지가 줄 끝에 붙습니다.
이름이 길면 배지에게 폭을 내주고
overflow대로 줄입니다 - 바 · 레일 밀도: 이름이 아이콘 아래(또는 위)로 쌓이고 배지가 아이콘 모서리에 걸립니다
-
높이: 내용만큼이고
itemMinHeight(사이드바 40, 바 · 레일 64)보다 낮아지지 않습니다 — 남는 높이를 채우지 않습니다. 이름을 숨긴 바 · 레일 목적지는 64×40 을 칠합니다 (iconOnlyItemMinWidth·iconOnlyItemMinHeight) -
포인터 영역: 바 · 레일 목적지는 칠한 크기와 상관없이 가로 · 세로 최소 48(
touchTargetSize)에서 탭을 받습니다. 히트 영역만 넓어지고 레이아웃은 그대로입니다 -
폭이 정해지지 않은 자리:
Row안처럼 폭이 열린 자리에서는 줄이 내용 폭이 되고 배지가 이름 바로 뒤에 붙습니다
상태 전환#
- 면 색은
duration(기본 150ms) 동안 바뀝니다. 동작 줄이기에서는 바로 바뀝니다
사용 가이드라인 (Usage Guidelines)#
Do#
선택은 화면이 하나만 가진다
NavigationBarItem(
label: 'Inbox',
selected: page == 0,
onPressed: () => setState(() => page = 0),
)
이유: 지금 있는 곳은 한 곳입니다. 화면의 상태 하나에서 모든 항목의 selected 를 계산하면 둘이
동시에 선택되는 일이 생기지 않습니다.
아이콘만 보일 때도 이름은 채운다
NavigationBarItem(
icon: const Icon(LucideIcons.settings),
label: 'Settings',
labelMode: CoreNavigationLabelMode.none,
onPressed: openSettings,
)
이유: 숨긴 이름은 스크린 리더가 읽는 이름이자 호버 툴팁입니다. label 이 없으면 "버튼" 으로만
읽힙니다.
Don't#
사이드바 줄을 손으로 그리지 않는다
// ❌ 면 · 상태 · 이름 · 배지를 직접 조립 — 테마와 상태 레이어가 닿지 않는다
Clickable(onPressed: go, child: Row(children: [icon, Text('Inbox'), badge]))
// ✅ 항목 위젯을 놓는다
NavigationBarItem(icon: icon, label: 'Inbox', badge: badge, onPressed: go)
이유: 손으로 그린 줄은 바의 스타일 · 밀도 · 상태 레이어 · 접근성 이름을 받지 못하고, 바가 바뀔 때 따라오지 않습니다.
접근성 (Accessibility)#
역할#
| Flutter | Web | |
|---|---|---|
| 항목 |
노드 하나 —
Clickable
이
button
·
enabled
·
selected
를 말하고, 항목은 이름(
label
)만 더하며 배지의 글자를 같은 노드에 합칩니다(
MergeSemantics
)
|
Clickable
이
role="button"
+
tabindex="0"
(활성) /
aria-disabled="true"
(비활성), 항목이
aria-labelledby
(이름 요소 → 배지 요소) + 선택 시
aria-current="page"
|
키보드#
| 키 | Flutter | Web |
|---|---|---|
Tab | 항목으로 | 항목으로 |
Enter / Space |
onPressed |
onPressed |
활성 항목마다 tab stop 이 하나씩 생깁니다. 비활성 항목은 포커스 대상이 아닙니다.
스크린 리더#
-
이름:
label다음에 배지의 글자(FlutterInbox\n3, WebInbox 3). 화면의 이름 글자는 겹쳐 읽히지 않습니다 - 현재 위치: Flutter 는
selected플래그, Web 은aria-current="page" - 선택 점은 장식입니다 — 선택은 이미 항목의 상태로 읽힙니다
- 상태 통보(live region)는 없습니다
포커스 관리#
-
진입 · 이탈 · 트랩: 없음. 포커스 표시는
Clickable이 소유합니다(FlutterFocusOutline, Web:focus-visible링)
알려진 제약#
- 화살표 키로 항목 사이를 옮기지 않습니다 — 항목마다 tab stop 이 생깁니다. 항목을 묶는 목록이 roving 포커스를 원하면 그 목록이 소유해야 합니다
-
label이 없는 항목은 이름이 없습니다 — 아이콘만 보이게 하려면label을 채우고labelMode: none으로 숨깁니다
전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | NavigationBarItem | NavigationBarItem |
| 현재 위치 | selected 시맨틱 플래그 | aria-current="page" |
| 이름 | 노드 하나에 이름 + 배지 글자 (줄바꿈으로 이음) | aria-labelledby (공백으로 이음) |
| 상태 레이어 | 상태별 면 색을 위젯이 고름 |
범위
<style>
규칙(
:hover
·
:focus-visible
·
:active
) — 단독 항목은 자기 규칙을, 바 안의 항목은 바가 한 번 씁니다
|
| 높이 | 정렬이 내용 높이에 맞춤(heightFactor: 1) | 블록 흐름 — 내용 높이 |
| Web 전용 HTML 표면 | — |
attributes
·
classes
·
css
·
eventHandlers
·
id
통과 (
Clickable
루트)
|
위 차이 밖에는 없습니다 — 이름 · 순서 · 기본값을 두 위젯 생성자에서 대조했습니다.
관련 컴포넌트 (Related Components)#
- NavigationBar: 항목 목록을 데이터로 받아 바 · 레일 · 사이드바를 통째로 그립니다 — 목적지마다 이 위젯을 합성합니다
- Drawer: 사이드바를 여닫는 서랍
- Badge:
badge에 놓는 개수
조합 예제#
openDrawer<void>(
context: context,
side: CoreDrawerSide.left,
builder: (drawerContext) => Drawer(
title: 'Workspace',
content: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: CoreSpace.space2,
children: [
NavigationBarItem(
icon: const Icon(LucideIcons.house),
label: 'Home',
selected: page == 0,
onPressed: () => go(0),
),
NavigationBarItem(
icon: const Icon(LucideIcons.inbox),
label: 'Inbox',
selected: page == 1,
onPressed: () => go(1),
),
],
),
),
)