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#
코드 보기
class BreadcrumbDefaultExample extends StatelessComponent {
const BreadcrumbDefaultExample({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) {},
);
}
}
class BreadcrumbDefaultExample extends StatelessWidget {
const BreadcrumbDefaultExample({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) {},
);
}
}
사용 시기 (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)#
역할#
| Flutter | Web | |
|---|---|---|
| 루트 |
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, Weboverflow-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)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Breadcrumb | Breadcrumb (동일) |
| 링크 처리 | onItemTap 콜백 |
onItemTap
콜백 +
href
시
data-href
속성 노출 (native
<a>
아님)
|
| 구분자 | chevron Icon | chevron Icon |
| 호버 | MouseRegion + 색 전환 | CSS :hover 토큰 클래스 |
관련 컴포넌트 (Related Components)#
- Navigation: 앱 주요 섹션 간 이동에 사용
- Tabs: 동일 계층의 콘텐츠를 탭으로 전환할 때 사용
- Link: 단일 이전 페이지 이동 링크가 필요할 때 사용