NavigationBar | CoUI
LogoCoUI

NavigationBar

가로 바 / 세로 레일 / 사이드바 세 가지 모드를 지원하는 통합 네비게이션 컨테이너 컴포넌트

NavigationBar#

NavigationBar는 주요 섹션 간 이동을 위한 통합 네비게이션 컨테이너입니다. mode 한 가지로 가로 바(bar), 세로 레일(rail), 확장 사이드바(sidebar) 세 가지 레이아웃을 모두 표현하며, 아이템은 단일 CoreNavigationBarItem 데이터 모델로 구성합니다.

Live Preview#

사용 시기 (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
backgroundColorsurfaceBright

모드 (Modes)#

Bar#

가로 바 레이아웃입니다. 모바일 앱 하단 네비게이션에 적합합니다.

NavigationBar(mode: CoreNavigationBarMode.bar, items: [...])

Rail#

세로 컴팩트 레일 레이아웃입니다. 측면 네비게이션에 적합합니다.

NavigationBar(mode: CoreNavigationBarMode.rail, items: [...])

확장 세로 사이드바 레이아웃입니다. 섹션 라벨과 구분선으로 계층 구조를 표현합니다.

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 을 생략하면 모드별 기본값(barnone/bottom, railselected/bottom, sidebarall/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)#

항목FlutterWeb
클래스명NavigationBarNavigationBar
렌더 루트 ColoredBox + SingleChildScrollView <nav> (flex)
라벨 오버플로TextOverflowCSS text-overflow
  • NavigationMenu: 드롭다운 패널이 있는 가로 메가 메뉴
  • Tabs: 화면 내 콘텐츠 섹션 전환에 사용