NavigationBarItem | CoUI
본문으로 건너뛰기
LogoCoUI

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#

구조 (Anatomy)#

사이드바 밀도(기본)에서는 한 줄입니다 — 아이콘과 이름이 시작 쪽에, 배지가 줄 끝에 붙습니다. 바 · 레일 밀도(mode: bar / rail)에서는 이름이 아이콘 아래로 쌓이고 배지가 아이콘의 위쪽 끝 모서리에 걸립니다.

부분역할슬롯
아이콘목적지의 그림. 없으면 이름만 그립니다icon
이름 목적지의 이름이자 접근성 이름. 숨겨도(labelMode) 이름과 툴팁으로 남습니다 label
배지개수 같은 표시. 이름 뒤에 이어 읽힙니다badge
면선택 · 호버 · 포커스 · 눌림을 칠하는 칸navigationBarStyle

Figma: ❖ NavigationBarItem

Figma 속성 대응#

Figma 세트의 축과 속성이 코드의 어느 값인지입니다. 이름이 다른 곳은 그대로 적어 둡니다 — Figma 쪽 이름은 디자이너가 정한 이름이라 코드가 바꾸지 않습니다.

Figma코드비고
Label (텍스트)label
Icon (인스턴스 교체)icon
Show iconicon 이 있는가끄면 이름만 있는 목적지
Show countbadge개수를 끄고 켜는 속성. 코드는 배지 위젯을 받습니다
Show mark selectionMark: dot 이름이 아이콘 아래 · 위로 쌓일 때만 그립니다
Selectedselected
State=disabledenabled: false
State=hover · focus—포인터와 키보드가 만드는 상태라 속성이 아닙니다
Label=end · bottom labelPosition end 는 사이드바 줄(mode: sidebar), bottom 은 바 · 레일
Label=nonelabelMode: 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 은 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 에서 onSurface 8 % 상태 레이어와 포커스 링을 그립니다

배치#

  • 사이드바 밀도: 줄이 받은 폭을 채우고, 아이콘과 이름이 시작 쪽에 붙고, 배지가 줄 끝에 붙습니다. 이름이 길면 배지에게 폭을 내주고 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)#

역할#

FlutterWeb
항목 노드 하나 — Clickable 이 button · enabled · selected 를 말하고, 항목은 이름( label )만 더하며 배지의 글자를 같은 노드에 합칩니다( MergeSemantics ) Clickable 이 role="button" + tabindex="0" (활성) / aria-disabled="true" (비활성), 항목이 aria-labelledby (이름 요소 → 배지 요소) + 선택 시 aria-current="page"

키보드#

키FlutterWeb
Tab항목으로항목으로
Enter / Space onPressed onPressed

활성 항목마다 tab stop 이 하나씩 생깁니다. 비활성 항목은 포커스 대상이 아닙니다.

스크린 리더#

  • 이름: label 다음에 배지의 글자(Flutter Inbox\n3, Web Inbox 3). 화면의 이름 글자는 겹쳐 읽히지 않습니다
  • 현재 위치: Flutter 는 selected 플래그, Web 은 aria-current="page"
  • 선택 점은 장식입니다 — 선택은 이미 항목의 상태로 읽힙니다
  • 상태 통보(live region)는 없습니다

포커스 관리#

  • 진입 · 이탈 · 트랩: 없음. 포커스 표시는 Clickable 이 소유합니다(Flutter FocusOutline, Web :focus-visible 링)

알려진 제약#

  • 화살표 키로 항목 사이를 옮기지 않습니다 — 항목마다 tab stop 이 생깁니다. 항목을 묶는 목록이 roving 포커스를 원하면 그 목록이 소유해야 합니다
  • label 이 없는 항목은 이름이 없습니다 — 아이콘만 보이게 하려면 label 을 채우고 labelMode: none 으로 숨깁니다

전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.

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

항목FlutterWeb
클래스명NavigationBarItemNavigationBarItem
현재 위치selected 시맨틱 플래그aria-current="page"
이름노드 하나에 이름 + 배지 글자 (줄바꿈으로 이음)aria-labelledby (공백으로 이음)
상태 레이어 상태별 면 색을 위젯이 고름 범위 <style> 규칙( :hover · :focus-visible · :active ) — 단독 항목은 자기 규칙을, 바 안의 항목은 바가 한 번 씁니다
높이정렬이 내용 높이에 맞춤(heightFactor: 1)블록 흐름 — 내용 높이
Web 전용 HTML 표면 — attributes · classes · css · eventHandlers · id 통과 ( Clickable 루트)

위 차이 밖에는 없습니다 — 이름 · 순서 · 기본값을 두 위젯 생성자에서 대조했습니다.

  • 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),
        ),
      ],
    ),
  ),
)