面包屑 breadcrumb
导航组件。三层同源:无头内核给出解剖与状态机,Vue 组件与自定义元素只是它的两层外壳,行为完全一致。
示例
基础用法
href 归作者写,末级只多一个 current:它拿到 aria-current="page"、点不动、也不占 Tab 位
<script setup lang="ts">
import {
XhBreadcrumbItem,
XhBreadcrumbLink,
XhBreadcrumbList,
XhBreadcrumbRoot,
XhBreadcrumbSeparator,
} from "@xihan-ui/vue";
</script>
<template>
<XhBreadcrumbRoot>
<XhBreadcrumbList>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/">首页</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/components">组件</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/components/breadcrumb" current>
面包屑
</XhBreadcrumbLink>
</XhBreadcrumbItem>
</XhBreadcrumbList>
</XhBreadcrumbRoot>
</template>折叠中间层级
省略号与分隔符同为 ol 的直接子 li,两者都对读屏隐藏,念出来仍是「列表,共 3 项」
<script setup lang="ts">
import {
XhBreadcrumbEllipsis,
XhBreadcrumbItem,
XhBreadcrumbLink,
XhBreadcrumbList,
XhBreadcrumbRoot,
XhBreadcrumbSeparator,
} from "@xihan-ui/vue";
</script>
<template>
<XhBreadcrumbRoot>
<XhBreadcrumbList>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/">首页</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>›</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/docs">文档</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>›</XhBreadcrumbSeparator>
<!-- 被折叠掉的那几层,只是视觉占位,不参与列表项计数 -->
<XhBreadcrumbEllipsis>…</XhBreadcrumbEllipsis>
<XhBreadcrumbSeparator>›</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/docs/deep/current" current>
当前页
</XhBreadcrumbLink>
</XhBreadcrumbItem>
</XhBreadcrumbList>
</XhBreadcrumbRoot>
</template>读屏文案
root 是 nav 地标,translations.root 换掉它的 aria-label,同页有多个地标时靠它区分
<script setup lang="ts">
import {
XhBreadcrumbItem,
XhBreadcrumbLink,
XhBreadcrumbList,
XhBreadcrumbRoot,
XhBreadcrumbSeparator,
} from "@xihan-ui/vue";
const translations = { root: "文章位置" };
</script>
<template>
<XhBreadcrumbRoot :translations="translations">
<XhBreadcrumbList>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/blog">博客</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/blog/2026">2026</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/blog/2026/design-system" current>
设计系统运行时
</XhBreadcrumbLink>
</XhBreadcrumbItem>
</XhBreadcrumbList>
</XhBreadcrumbRoot>
</template>语气
tone 换的是当前项的文字色,以及可点那几层悬停时的文字色;末级预置为当前项
brand
neutral
success
warning
danger
info
<script setup lang="ts">
import {
XhBreadcrumbItem,
XhBreadcrumbLink,
XhBreadcrumbList,
XhBreadcrumbRoot,
XhBreadcrumbSeparator,
} from "@xihan-ui/vue";
const tones = ["brand", "neutral", "success", "warning", "danger", "info"] as const;
</script>
<template>
<div style="inline-size: 100%; display: grid; gap: 12px">
<div
v-for="t in tones"
:key="t"
style="display: flex; align-items: center; gap: 12px"
>
<span style="inline-size: 80px; flex: none; font-size: 12px">{{ t }}</span>
<XhBreadcrumbRoot :tone="t">
<XhBreadcrumbList>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/">首页</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/components">组件</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/components/breadcrumb" current>
面包屑
</XhBreadcrumbLink>
</XhBreadcrumbItem>
</XhBreadcrumbList>
</XhBreadcrumbRoot>
</div>
</div>
</template>尺寸
size 换整条路径的字号与各层之间的间距,不传 size 即默认档
小
默认
大
<script setup lang="ts">
import {
XhBreadcrumbItem,
XhBreadcrumbLink,
XhBreadcrumbList,
XhBreadcrumbRoot,
XhBreadcrumbSeparator,
} from "@xihan-ui/vue";
// 中间一档不写 size,用 undefined 表达
const sizes = [
{ size: "sm", label: "小" },
{ size: undefined, label: "默认" },
{ size: "lg", label: "大" },
] as const;
</script>
<template>
<div style="inline-size: 100%; display: grid; gap: 16px">
<div
v-for="s in sizes"
:key="s.label"
style="display: flex; align-items: center; gap: 12px"
>
<span style="inline-size: 40px; flex: none; font-size: 12px">
{{ s.label }}
</span>
<XhBreadcrumbRoot :size="s.size">
<XhBreadcrumbList>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/">首页</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/components">组件</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/components/breadcrumb" current>
面包屑
</XhBreadcrumbLink>
</XhBreadcrumbItem>
</XhBreadcrumbList>
</XhBreadcrumbRoot>
</div>
</div>
</template>层级下拉
某一层要换去处时,把菜单整套放进 item 里;面包屑只管这一层的排版
<script setup lang="ts">
import { ref } from "vue";
import {
XhBreadcrumbItem,
XhBreadcrumbLink,
XhBreadcrumbList,
XhBreadcrumbRoot,
XhBreadcrumbSeparator,
XhMenuContent,
XhMenuItem,
XhMenuPositioner,
XhMenuRoot,
XhMenuTrigger,
} from "@xihan-ui/vue";
const projects = [
{ value: "web", label: "官网" },
{ value: "admin", label: "后台" },
{ value: "mobile", label: "移动端" },
];
const current = ref("admin");
function onSelect(details: { value: string }): void {
current.value = details.value;
}
</script>
<template>
<XhBreadcrumbRoot>
<XhBreadcrumbList>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/">工作台</XhBreadcrumbLink>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<!-- 这一层不是链接而是一组可切换的去处 -->
<XhMenuRoot @select="onSelect">
<XhMenuTrigger>
{{ projects.find((p) => p.value === current)?.label }} ▾
</XhMenuTrigger>
<XhMenuPositioner>
<XhMenuContent>
<XhMenuItem v-for="p in projects" :key="p.value" :value="p.value">
{{ p.label }}
</XhMenuItem>
</XhMenuContent>
</XhMenuPositioner>
</XhMenuRoot>
</XhBreadcrumbItem>
<XhBreadcrumbSeparator>/</XhBreadcrumbSeparator>
<XhBreadcrumbItem>
<XhBreadcrumbLink href="#/settings" current>设置</XhBreadcrumbLink>
</XhBreadcrumbItem>
</XhBreadcrumbList>
</XhBreadcrumbRoot>
</template>产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-breadcrumb> |
| Vue 组件 | XhBreadcrumbEllipsis XhBreadcrumbItem XhBreadcrumbLink XhBreadcrumbList XhBreadcrumbRoot XhBreadcrumbSeparator |
| 组合式函数 | useBreadcrumb |
| 状态机 | 无,connect 直接由 props 算属性 |
| 皮肤 | @xihan-ui/styles/breadcrumb.css |
解剖
部件名即 data-part 属性值,也是皮肤的选择器。加粗的是必备部件,不渲染它组件不工作(Web Components 适配器会在诊断通道上报 wc.missing-part)。
data-scope="breadcrumb":root · list · item · link · separator · ellipsis
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
dir | Direction | 文字方向,只作用于排版;作者没给就不写。 | |
size | Size | 尺寸:sm / md / lg。 | |
tone | Tone | 语气:brand / neutral / success / warning / danger / info,决定用哪族颜色。 | |
translations | Partial<BreadcrumbTranslations> |
connect API
useBreadcrumb 产出的对象。getXxxProps() 铺到对应部件的宿主元素上,其余是可读状态与操作入口。
| 成员 | 类型 | 说明 |
|---|---|---|
getRootProps | () => T['element'] | |
getListProps | () => T['element'] | |
getItemProps | () => T['element'] | |
getLinkProps | (props: BreadcrumbLinkProps) => T['element'] | |
getSeparatorProps | () => T['element'] | |
getEllipsisProps | () => T['element'] |
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Enter | focus in link, 非当前页 | 跟随链接(原生 <a href> 的激活行为,面包屑自己不监听按键) |
Tab / Shift+Tab | focus in root | 逐条走过可点的链接;面包屑不做 roving tabindex,当前页那条带 tabindex=-1 自动脱序 |
