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

Breadcrumb

현재 페이지의 위치를 계층적으로 나타내는 탐색 경로 컴포넌트

목차

현재 페이지의 위치를 계층적으로 표시하여 사용자가 상위 페이지로 쉽게 이동할 수 있도록 하는 탐색 경로 컴포넌트입니다.

기본 사용법 (Basic Usage)#

Flutter / Web 모두 동일한 Breadcrumb API 를 사용합니다. 데이터-드리븐 — CoreBreadcrumbItem 리스트를 넘기면 마지막 항목이 자동으로 현재 위치(비탐색) 로 렌더링됩니다.

// 기본 브레드크럼
Breadcrumb(
  items: const [
    CoreBreadcrumbItem(label: '홈', href: '/'),
    CoreBreadcrumbItem(label: '카테고리', href: '/category'),
    CoreBreadcrumbItem(label: '상품 상세'),
  ],
  onItemTap: handleBreadcrumbNavigation,
)
// 기본 브레드크럼
Breadcrumb(
  items: const [
    CoreBreadcrumbItem(label: '홈', href: '/'),
    CoreBreadcrumbItem(label: '카테고리', href: '/category'),
    CoreBreadcrumbItem(label: '상품 상세'),
  ],
  onItemTap: handleBreadcrumbNavigation,
)

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 3단계 이상의 계층 구조를 가진 페이지에서 현재 위치를 안내할 때
  • 사용자가 상위 카테고리로 빠르게 돌아갈 수 있어야 할 때 (예: 이커머스 카테고리, 관리자 설정)
  • 문서, 파일 탐색기처럼 깊은 계층 구조를 탐색할 때

대신 다른 컴포넌트를 사용하세요:

  • Tabs: 부모-자식 경로가 아니라 동급 패널 하나를 전환할 때
  • NavigationBar: 계층 경로 대신 앱의 주요 섹션 선택·이동이 필요할 때
  • Link: 항목 목록 없이 목적지 하나에 대한 링크가 필요할 때

Props / Parameters#

속성타입기본값설명
items List<CoreBreadcrumbItem> 필수 탐색 경로 항목 목록 (마지막 = 현재)
onItemTap void Function(int)? null 비탐색(현재) 외 항목 탭 콜백 — 0-based 인덱스
enabled bool? null null 이면 onItemTap != null 에서 유도 ( enabled ?? (onItemTap != null) ). 비활성이면 모든 항목 글자와 구분자가 disabledColor 로 그려지고 어떤 항목도 상호작용하지 않습니다
variant CoreBreadcrumbVariant standard 시각 변형
breadcrumbStyle CoreBreadcrumbStyle? null 인스턴스 스타일 (Style 시스템 참조)

CoreBreadcrumbItem 은 label (필수) 과 href (선택) 를 가집니다. 두 플랫폼 모두 탐색은 onItemTap 콜백으로 처리됩니다 — href 는 정보 제공용입니다. Web 에서는 상호작용 가능한 항목에 data-href 속성으로 노출될 뿐(툴링용), 네이티브 <a> 태그로 렌더링되지 않습니다.

스타일 시스템 (Style System)#

Breadcrumb 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreBreadcrumbStyle 단일 슬롯으로 흐릅니다. 시맨틱 / behaviour 필드 (items / onItemTap / enabled / variant) 는 위젯/컴포넌트 파라미터로 직접 전달합니다.

시맨틱 vs 스타일#

  • 시맨틱 / behaviour: 위젯/컴포넌트 파라미터로 직접 (items, onItemTap, enabled, variant)
  • chrome / dimensional / 슬롯 스타일: CoreBreadcrumbStyle 한 곳으로 (itemSeparatorGapStyle / itemColor / currentItemColor / itemHoverColor / disabledColor / itemTextStyle / currentItemTextStyle / itemPadding / separatorIconStyle / hoverDuration / clickableStyle)

Resolve chain#

CoreBreadcrumbStyle.defaultX                   // design system default
  → CoreBreadcrumbTheme.style                  // 프로젝트 공통
  → widget.breadcrumbStyle                     // 인스턴스별

CoreBreadcrumbStyle 필드#

필드타입설명
itemSeparatorGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between item / separator / item — forwarded straight to Gap(gapStyle: …) . null defers to [defaultItemSeparatorGapStyle].
itemColor CoreColor? Override colour applied to inactive (link) items. null defers to [itemTextStyle]'s colour, then [defaultItemColor].
currentItemColor CoreColor? Override colour applied to the current (last) item.
itemHoverColor CoreColor? Override hover colour applied to an interactive link item. null defers to [defaultItemHoverColor].
disabledColor CoreColor? Override colour of every item label and of the separator while the breadcrumb is disabled. null defers to [defaultDisabledColor].
itemTextStyle CoreTextStyle? Item text style — applies to every breadcrumb item (link and current). null defers to [defaultItemTextStyle] for the links.
currentItemTextStyle CoreTextStyle? Text style of the current (last) item, layered on top of [itemTextStyle]. null defers to [defaultCurrentItemTextStyle].
itemPadding CoreEdgeInsets? Inset around each item label (inside the focus ring). null defers to [defaultItemPadding].
separatorIconStyle CoreIconStyle? Separator icon style (size / colour of the chevron glyph).
hoverDuration Duration? Override colour-transition duration for link hover. null defers to [defaultHoverDuration].
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed per-item Clickable (interactive link items). Merged on top of [defaultClickableStyle] and raw-forwarded to Clickable(clickableStyle:) .

빠른 오버라이드 (Chain)#

이미 만든 Breadcrumb 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class BreadcrumbChainExample extends StatelessWidget {
  const BreadcrumbChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Breadcrumb(
      items: const [
        CoreBreadcrumbItem(label: 'Home', href: '/'),
        CoreBreadcrumbItem(label: 'Components', href: '/components'),
        CoreBreadcrumbItem(label: 'Breadcrumb'),
      ],
      onItemTap: (index) {},
    ).withStyle(
      const CoreBreadcrumbStyle(
        itemSeparatorGapStyle: CoreGapStyle(size: CoreSpace.space16),
        itemColor: CoreColor.token(CoreColors.onSurfaceVariant),
        currentItemColor: CoreColor.token(CoreColors.primary),
        itemHoverColor: CoreColor.token(CoreColors.tertiary),
        separatorIconStyle: CoreIconStyle(
          size: CoreSize.size12,
          color: CoreColor.token(CoreColors.outline),
        ),
      ),
    );
  }
}
class BreadcrumbChainExample extends StatelessComponent {
  const BreadcrumbChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return Breadcrumb(
      items: const [
        CoreBreadcrumbItem(label: 'Home', href: '/'),
        CoreBreadcrumbItem(label: 'Components', href: '/components'),
        CoreBreadcrumbItem(label: 'Breadcrumb'),
      ],
      onItemTap: (index) {},
    ).withStyle(
      const CoreBreadcrumbStyle(
        itemSeparatorGapStyle: CoreGapStyle(size: CoreSpace.space16),
        itemColor: CoreColor.token(CoreColors.onSurfaceVariant),
        currentItemColor: CoreColor.token(CoreColors.primary),
        itemHoverColor: CoreColor.token(CoreColors.tertiary),
        separatorIconStyle: CoreIconStyle(
          size: CoreSize.size12,
          color: CoreColor.token(CoreColors.outline),
        ),
      ),
    );
  }
}

변형 (Variants)#

Breadcrumb 은 현재 단일 시각 변형(standard) 을 가집니다. chevron(›) 구분자가 기본이며, separator 아이콘 크기 / 색은 breadcrumbStyle.separatorIconStyle 로 오버라이드합니다.

Breadcrumb(
  items: const [
    CoreBreadcrumbItem(label: '대시보드', href: '/'),
    CoreBreadcrumbItem(label: '프로젝트', href: '/projects'),
    CoreBreadcrumbItem(label: 'CoUI'),
  ],
  breadcrumbStyle: const CoreBreadcrumbStyle(
    itemSeparatorGapStyle: CoreGapStyle(size: CoreSpace.space12),
  ),
)

동작 스펙 (Behavior)#

인터랙션#

  • 클릭/탭: 마지막 외 항목 클릭 시 onItemTap 콜백을 0-based 인덱스와 함께 호출 (마지막 항목은 현재 페이지이므로 비활성)
  • onItemTap 이 없으면 enabled 도 false 로 유도되어 앞 항목도 표시 전용이고, 비활성 색(disabledColor)으로 그려집니다. href 만 제공해도 자체 이동은 하지 않습니다. 핸들러 없이 평소 색으로 표시만 하려면 enabled: true 를 명시합니다. 빈 목록은 경로 항목·구분자를 그리지 않습니다.
  • 호버: 클릭 가능한 항목 호버 시 텍스트 색이 onSurface 로 전환 (150ms ease-in-out)

상태 전환#

  • 마지막 항목은 현재 위치를 나타내며 onSurface · labelLarge(14 / Medium) 비탐색 텍스트로 표시
  • 앞 항목들은 onSurfaceVariant · bodyMedium(14 / Regular) 링크 스타일로 표시 (클릭 가능). 구분자 chevron 은 onSurfaceSubtle
  • 각 항목은 위아래 2 · 좌우 4 의 안쪽 여백(itemPadding)을 가지며, 포커스 링은 그 여백까지 감싼 항목 박스(모서리 6 → 링 모서리 10)에 그려집니다. 항목 사이 간격은 4 입니다
  • 비활성(enabled: false)이면 모든 항목 글자와 구분자가 disabledColor(disabled-content)로 그려지고 호버 · 포커스 · 탭에 반응하지 않습니다. 호출부가 적은 색(itemColor 등)보다 비활성이 우선합니다

애니메이션#

  • 링크 호버 시 색상 전환 150ms ease-in-out

사용 가이드라인 (Usage Guidelines)#

✅ Do#

현재 페이지(마지막 항목)는 자동으로 클릭 불가로 표시

Breadcrumb(
  items: const [
    CoreBreadcrumbItem(label: '홈', href: '/'),
    CoreBreadcrumbItem(label: '설정', href: '/settings'),
    CoreBreadcrumbItem(label: '보안'), // 마지막 = 현재 페이지
  ],
  onItemTap: handleNavigation,
)

마지막 항목은 자동으로 비탐색 텍스트로 렌더링되어 현재 위치를 명확히 한다.


❌ Don't#

2단계 이하 계층에서 브레드크럼 남용하지 않기

// ❌ 단순 2단계 구조에 브레드크럼
Breadcrumb(
  items: const [
    CoreBreadcrumbItem(label: '홈', href: '/'),
    CoreBreadcrumbItem(label: '설정'),
  ],
)

2단계 이하는 뒤로가기 버튼 또는 간단한 Link로 충분하며 브레드크럼이 오히려 복잡도를 높인다.

✅ Do#

경로가 길면 좁은 화면에서 가로 스크롤로 흘려보내기

Breadcrumb 은 콘텐츠가 컨테이너를 넘으면 가로 스크롤(overflow-x-auto)로 처리하므로 줄바꿈으로 레이아웃이 깨지지 않는다.

접근성 (Accessibility)#

역할#

FlutterWeb
루트 CoUISemantics(role: .navigation, label: …, container: true) — <nav> 에 대응하는 navigation 랜드마크 <div role="navigation" aria-label="…">
이동 가능한 항목 Clickable 의 Semantics(button: true) 위에 컴포넌트가 Semantics(link: true) — 두 플래그가 함께 설정됩니다 Clickable 의 role="button" 을 caller attributes 가 role="link" 로 덮습니다. href 는 data-href 속성으로만 노출되고 실제 <a> 가 아닙니다
마지막(현재 위치) 항목 별도 표시 없음 — 색과 굵기만 다른 일반 텍스트입니다 aria-current="page"
비활성 (enabled: false) 항목이 Clickable 로 감싸이지 않으므로 포커스 · 탭 대상에서 빠집니다 루트 <div role="navigation"> 에 aria-disabled="true" , 항목은 role="link" 없이 렌더됩니다
구분자(chevron) 이름 없는 image 시맨틱 노드가 생깁니다 — SVG 가 excludeFromSemantics 기본값(false)으로 렌더되기 때문입니다 <span aria-hidden="true"> 로 명시적으로 제외됩니다

랜드마크 라벨은 두 플랫폼 모두 breadcrumbLabel 로케일 키에서 옵니다(한국어 기본값 "탐색 경로", 영어 기본값 "Breadcrumb") — 하드코딩된 문자열이 아니라 활성 로케일을 따라갑니다. 단 Web 에서는 role 과 aria-label 이 caller attributes 를 병합한 뒤에 기록되므로 호출부에서 덮을 수 없습니다.

키보드#

키동작
Tab다음 이동 가능한 항목으로 포커스 이동
Shift + Tab이전 항목으로 포커스 이동
Enter포커스된 항목 활성화 (onItemTap 호출)
Space포커스된 항목 활성화
Arrow (←→) 컴포넌트는 처리하지 않습니다. 네이티브 Flutter 에서는 프레임워크 기본값이 방향 포커스 이동으로 해석하고, Flutter Web 과 Web(Jaspr) 에서는 각각 스크롤 / 무동작으로 갑니다

Enter / Space 활성화는 두 플랫폼 모두 합성된 Clickable 이 소유합니다.

스크린 리더#

  • 이름의 출처는 CoreBreadcrumbItem.label 이며 필수 파라미터라 이름 없는 항목이 생기지 않습니다. 두 플랫폼 모두 렌더된 텍스트가 그대로 접근 가능한 이름이 됩니다.
  • 현재 위치: Web 은 마지막 항목에 aria-current="page" 를 붙입니다. Flutter 에는 대응 표시가 없습니다 — 아래 알려진 제약 참조.
  • 구분자: Web 은 aria-hidden 으로 접근성 트리에서 제외합니다. Flutter 는 제외하지 않습니다 — SVG 가 excludeFromSemantics 기본값으로 렌더되어 항목 사이마다 이름 없는 image 노드가 하나씩 남습니다.
  • onItemTap 이 없거나 enabled: false 이면 모든 항목이 비대화형 텍스트가 되어 link / role="link" 도 붙지 않습니다. 비활성은 색만이 아니라 상호작용이 없다는 사실로도 전달됩니다(Web 은 aria-disabled).
  • live region 없음 — items 가 바뀌어도 그 변화는 통보되지 않습니다.

포커스 관리#

  • 진입 / 이탈: 오버레이가 아니므로 마운트·언마운트 시 포커스를 옮기지 않습니다.
  • 트랩: 없습니다. Tab 은 마지막 항목을 지나 바깥으로 나갑니다.
  • tab stop 수: 이동 가능한 항목마다 하나씩 생깁니다. 마지막(현재 위치) 항목과 구분자는 포커스 대상이 아니므로, 항목이 N 개면 tab stop 은 N−1 개입니다.
  • 스크롤 추종: 트레일은 가로로 스크롤됩니다(Flutter SingleChildScrollView, Web overflow-x-auto). 화면 밖 항목으로 포커스가 이동하면 시야 안으로 끌려옵니다.
  • 포커스 표시: Clickable 이 소유합니다 — Flutter 는 FocusOutline, Web 은 :focus-visible 링입니다. 다만 이 컴포넌트는 pressedScale: 1 로 눌림 축소 피드백만 꺼두므로 포커스 링 자체는 기본값 그대로 살아 있습니다.

알려진 제약#

  • Flutter 에서 현재 위치가 통보되지 않습니다. Web 은 마지막 항목에 aria-current="page" 를 붙이지만 Flutter 항목은 색(currentItemColor ↔ itemColor)만 달라집니다 — 색은 스크린 리더에게 아무 정보가 아니고, OS 의 색 강제(forced-colors) 모드에서는 시각적 구분마저 사라집니다. 대안: 호출부에서 마지막 항목을 감싸 Semantics 를 직접 얹는 것 외에 컴포넌트 API 에는 진입점이 없습니다.
  • 항목의 포인터 타깃은 렌더된 박스와 같습니다. 항목에는 터치 타깃 확장 마커가 배선되어 있지 않아 히트 영역이 안쪽 여백까지 포함한 항목 박스입니다. 링크 항목의 높이는 bodyMedium(14px × line-height 1.5 = 21px)에 위아래 여백 4 를 더한 25px 라 데스크톱 배율(1.0)과 기본 브라우저 글꼴 크기에서 CoreTouchTarget.minimum(24)을 넘지만, 폭은 라벨 길이에 따라 24px 아래일 수 있습니다. 히트 영역만 따로 넓히는 경로는 없습니다 — itemPadding 으로 박스 자체를 키웁니다.
  • Web 에서 hover 색 전환에 '동작 줄이기' 설정이 닿지 않습니다. Web resolver 가 transition-duration 을 --coui-duration-* 변수가 아니라 리터럴 Nms 로 내보내기 때문에, 토큰 값을 그대로 쓰더라도 @media (prefers-reduced-motion: reduce) 재정의를 지나갑니다. Flutter 는 같은 값을 resolveMotionDuration 으로 통과시켜 정상적으로 억제됩니다. 대안: hoverDuration: Duration.zero 로 호출부에서 직접 끄는 것 외에 경로가 없습니다.
  • Web 항목은 진짜 링크가 아닙니다. role="link" 는 붙지만 href 는 data-href 로만 노출되므로 브라우저의 링크 어포던스(새 탭 열기 · 상태 표시줄의 URL 미리보기 · 링크 목록 탐색의 목적지)가 없습니다. 이동은 onItemTap 콜백으로만 일어납니다.

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

항목FlutterWeb
클래스명BreadcrumbBreadcrumb (동일)
링크 처리 onItemTap 콜백 onItemTap 콜백 + href 시 data-href 속성 노출 (native <a> 아님)
구분자chevron Iconchevron Icon
호버MouseRegion + 색 전환CSS :hover 토큰 클래스
  • Navigation: 앱 주요 섹션 간 이동에 사용
  • Tabs: 동일 계층의 콘텐츠를 탭으로 전환할 때 사용
  • Link: 단일 이전 페이지 이동 링크가 필요할 때 사용