底部标签栏TabBar
底部主导航,2~5 个标签页之间切换,是移动端一级导航
导航 规格 2 · 底部标签栏 TabBar 6 端实现 触摸优先
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">
<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/tabbar.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/TabBar.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
最常见的三项导航:图标 + 文字等分整栏宽度,热区高度 ≥ 44px。
查看代码(演示页原文 · 24 行)
<section class="demo-block" data-demo="count-3">
<p class="demo-label">三项基础(count=3 / badge=none,点击切换标签)</p>
<div class="demo-frame" data-assert="tabbar-count-3">
<div class="demo-frame__body" id="panel-home" data-panel>首页内容区(tab-switches 断言的可见面板)</div>
<div class="demo-frame__body demo-panel" id="panel-order" data-panel hidden>订单内容区</div>
<div class="demo-frame__body demo-panel" id="panel-mine" data-panel hidden>我的内容区</div>
<nav class="kole-m-tabbar" role="tablist" aria-label="主导航">
<button class="kole-m-tabbar__item is-active" type="button" role="tab" aria-selected="true"
data-panel="home">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H10v6H4a1 1 0 0 1-1-1z"/></svg>
<span class="kole-m-tabbar__label">首页</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false" data-panel="order"
data-behavior="tab-switches:data-panel">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 4h14v16H5z"/><path d="M9 8h6M9 12h6"/></svg>
<span class="kole-m-tabbar__label">订单</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false" data-panel="mine">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.5 3-6 7-6s7 2.5 7 6"/></svg>
<span class="kole-m-tabbar__label">我的</span>
</button>
</nav>
</div>
</section>角标宽度随位数增长;超过两位数按业务约定收敛(如 99+)。
查看代码(演示页原文 · 26 行)
<section class="demo-block" data-demo="count-4-badge">
<p class="demo-label">四项 + 数字角标(count=4 / badge=number)</p>
<div class="demo-frame" data-assert="tabbar-count-4-badge-number">
<div class="demo-frame__body">角标为数字形态,宽度随位数增长。</div>
<nav class="kole-m-tabbar" role="tablist" aria-label="四项导航">
<button class="kole-m-tabbar__item is-active" type="button" role="tab" aria-selected="true">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/></svg>
<span class="kole-m-tabbar__label">首页</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 4h14v16H5z"/></svg>
<span class="kole-m-tabbar__label">订单</span>
<span class="kole-m-tabbar__badge">12</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
<span class="kole-m-tabbar__label">消息</span>
<span class="kole-m-tabbar__badge">99+</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.5 3-6 7-6s7 2.5 7 6"/></svg>
<span class="kole-m-tabbar__label">我的</span>
</button>
</nav>
</div>
</section>五项是上限,标签文字仍不换行;红点用于「有更新但不必计数」。
查看代码(演示页原文 · 16 行)
<section class="demo-block" data-demo="count-5-dot">
<p class="demo-label">五项 + 红点(count=5 / badge=dot)</p>
<div class="demo-frame" data-assert="tabbar-count-5-badge-dot">
<div class="demo-frame__body">五项时标签文字仍不换行,宽度等分。</div>
<nav class="kole-m-tabbar" role="tablist" aria-label="五项导航">
<button class="kole-m-tabbar__item is-active" type="button" role="tab" aria-selected="true"><span class="kole-m-tabbar__label">首页</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false"><span class="kole-m-tabbar__label">分类</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false">
<span class="kole-m-tabbar__label">消息</span>
<span class="kole-m-tabbar__badge is-dot" aria-label="有新消息"></span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false"><span class="kole-m-tabbar__label">购物车</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false"><span class="kole-m-tabbar__label">我的</span></button>
</nav>
</div>
</section>02 组件状态
禁用项置灰、保留位置、不参与切换(无权限模块的常见表达)。
查看代码(演示页原文 · 12 行)
<section class="demo-block" data-demo="disabled">
<p class="demo-label">含禁用项(disabled 置灰且不响应)</p>
<div class="demo-frame" data-assert="tabbar-disabled">
<div class="demo-frame__body">禁用项保留位置,不参与切换。</div>
<nav class="kole-m-tabbar" role="tablist" aria-label="含禁用项导航">
<button class="kole-m-tabbar__item is-active" type="button" role="tab" aria-selected="true"><span class="kole-m-tabbar__label">首页</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false" disabled aria-label="订单(无权限)"><span class="kole-m-tabbar__label">订单</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false"><span class="kole-m-tabbar__label">我的</span></button>
</nav>
</div>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
items | Item[] | [] | 标签项:{ key, label, icon?, badge?, dot?, disabled?, ariaLabel? }(规格 §2.2) | N |
value | string | '' | 当前选中项的 key(受控,规格 §2.4) | N |
label | string | '主导航' | 整栏的无障碍名称(role=tablist 的 aria-label,规格 §2.6) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
change | key: string | 切换标签时触发,参数为选中项的 key |
插槽
| 名称 | 说明 |
|---|---|
— | 无插槽;项由 items 驱动(uni-app 端的图标由宿主以图片/slot 提供,规格 §2.7) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
| 本组件没有组件级 CSS 变量 | ||
何时使用
- 底部主导航,2~5 个标签页之间切换,是移动端一级导航
- 固定于页面底部,高度 50px + 底部安全区
- 每项点击热区等分整栏宽度,高度不小于 44px
- 选中项切换后 aria-selected 同步变化
- 底部内边距包含安全区
- 标签文字 11px,图标可为内联 SVG
交互与触控
- 每项点击热区等分整栏宽度,高度不小于 44px
- 选中项切换后 aria-selected 同步变化
- 底部内边距包含安全区
无障碍
- 容器 role=tablist,单项 role=tab
- 选中项 aria-selected=true,未选中 false
- 每项具备 aria-label(图标 + 文字时可用文字代替)
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 顶部导航栏NavBar | 顶栏给标题与返回,标签栏给主模块切换;两者一起构成一级框架 |
| 下拉刷新PullRefresh | 都在页面上做纵向处理,注意手势与底部导航栏不要抢事件 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 标签项超过 5 个时的滚动或折叠规则 |
| 禁止发明 | 图标资源的成套规则(规格只约定尺寸与语义) |
| 规格未定 | 角标超过两位数的收敛(如 99+) |
| 规格未定 | 选中态是否带图标填充切换 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
bar | 标签栏容器,固定于页面底部,高度 50px + 底部安全区 |
item | 单个标签,含图标与文字 |
icon | 图标,可为内联 SVG |
label | 标签文字,11px |
badge | 角标,可为数字或红点 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
count | 2 / 3 / 4 / 5 | 2 (.kole-m-tabbar__item) 3 (.kole-m-tabbar__item) 4 (.kole-m-tabbar__item) 5 (.kole-m-tabbar__item) |
badge | none / dot / number | none (由数据驱动,无专属类) dot .kole-m-tabbar__badge .is-dot number .kole-m-tabbar__badge |
代表变体
| 变体 | 标签 |
|---|---|
count=3 · badge=none | 三项基础 |
count=4 · badge=number | 四项 + 数字角标 |
count=5 · badge=dot | 五项 + 红点 |
count=4 · badge=dot | 含禁用项 |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/TabBar.css · 纯样式(CSS) · 97 行
/* Kole UI Mobile · TabBar 样式 — 对齐移动端规格 §2
底部标签栏:固定于页面底部,50px + 底部安全区;每项点击热区等分整栏宽度且高度 ≥ 44px。 */
.kole-m-tabbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 90;
display: flex;
box-sizing: border-box;
height: calc(var(--kole-m-tabbar-height) + var(--kole-m-safe-bottom));
padding-bottom: var(--kole-m-safe-bottom);
background: var(--kole-color-card-bg);
border-top: 1px solid var(--kole-color-border);
font-family: var(--kole-font-family);
}
.kole-m-tabbar__item {
position: relative;
flex: 1 1 0;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
box-sizing: border-box;
min-height: var(--kole-m-touch-target);
padding: var(--kole-space-4) 0;
border: 0;
background: transparent;
color: var(--kole-color-text-secondary);
font-family: inherit;
font-size: var(--kole-m-font-size-caption);
line-height: 1.2;
cursor: pointer;
touch-action: manipulation;
}
.kole-m-tabbar__item:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: -2px;
}
.kole-m-tabbar__item:active { background: var(--kole-color-brand-bg); }
.kole-m-tabbar__item.is-active { color: var(--kole-color-brand); }
.kole-m-tabbar__item:disabled,
.kole-m-tabbar__item.is-disabled {
color: var(--kole-color-text-disabled);
cursor: not-allowed;
}
.kole-m-tabbar__item:disabled:active { background: transparent; }
.kole-m-tabbar__icon {
display: block;
width: var(--kole-icon-size-20);
height: var(--kole-icon-size-20);
}
.kole-m-tabbar__label {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 角标:number 形态显示数字,dot 形态只显示红点 */
.kole-m-tabbar__badge {
position: absolute;
top: 2px;
inset-inline-start: 50%;
margin-inline-start: 2px;
box-sizing: border-box;
min-width: 16px;
height: 16px;
padding: 0 var(--kole-space-4);
border-radius: 8px;
background: var(--kole-color-error);
color: var(--kole-color-text-inverse);
font-family: var(--kole-font-family-num);
font-size: 10px;
line-height: 16px;
text-align: center;
}
.kole-m-tabbar__badge.is-dot {
min-width: 8px;
width: 8px;
height: 8px;
padding: 0;
border-radius: 50%;
}
frameworks-mobile/TabBar.html · H5 原生(无框架) · 154 行
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Kole UI Mobile · TabBar(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="TabBar.css">
<style>
body { margin: 0; background: var(--kole-color-page-bg); font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
.demo { max-width: 375px; margin: 0 auto; padding: var(--kole-m-gutter) 0; }
.demo-label { margin: 0; padding: var(--kole-space-12) var(--kole-m-gutter) var(--kole-space-8);
font-size: var(--kole-m-font-size-label); color: var(--kole-color-text-secondary); }
/* 展示框:transform 让内部 position:fixed 以本框为包含块,
这样同一页可并列展示多个「固定底部」形态;生产环境直接落在视口底部。 */
.demo-frame { position: relative; max-width: 375px; margin: 0 auto; height: 180px;
overflow: hidden; transform: translateZ(0); background: var(--kole-color-card-bg);
border-block: 1px solid var(--kole-color-border); }
.demo-frame__body { padding: var(--kole-m-gutter); font-size: var(--kole-m-font-size-label);
color: var(--kole-color-text-secondary); line-height: 1.7; }
.demo-panel[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="count-3">
<p class="demo-label">三项基础(count=3 / badge=none,点击切换标签)</p>
<div class="demo-frame" data-assert="tabbar-count-3">
<div class="demo-frame__body" id="panel-home" data-panel>首页内容区(tab-switches 断言的可见面板)</div>
<div class="demo-frame__body demo-panel" id="panel-order" data-panel hidden>订单内容区</div>
<div class="demo-frame__body demo-panel" id="panel-mine" data-panel hidden>我的内容区</div>
<nav class="kole-m-tabbar" role="tablist" aria-label="主导航">
<button class="kole-m-tabbar__item is-active" type="button" role="tab" aria-selected="true"
data-panel="home">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H10v6H4a1 1 0 0 1-1-1z"/></svg>
<span class="kole-m-tabbar__label">首页</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false" data-panel="order"
data-behavior="tab-switches:data-panel">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 4h14v16H5z"/><path d="M9 8h6M9 12h6"/></svg>
<span class="kole-m-tabbar__label">订单</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false" data-panel="mine">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.5 3-6 7-6s7 2.5 7 6"/></svg>
<span class="kole-m-tabbar__label">我的</span>
</button>
</nav>
</div>
</section>
<section class="demo-block" data-demo="count-4-badge">
<p class="demo-label">四项 + 数字角标(count=4 / badge=number)</p>
<div class="demo-frame" data-assert="tabbar-count-4-badge-number">
<div class="demo-frame__body">角标为数字形态,宽度随位数增长。</div>
<nav class="kole-m-tabbar" role="tablist" aria-label="四项导航">
<button class="kole-m-tabbar__item is-active" type="button" role="tab" aria-selected="true">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/></svg>
<span class="kole-m-tabbar__label">首页</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 4h14v16H5z"/></svg>
<span class="kole-m-tabbar__label">订单</span>
<span class="kole-m-tabbar__badge">12</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
<span class="kole-m-tabbar__label">消息</span>
<span class="kole-m-tabbar__badge">99+</span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false">
<svg class="kole-m-tabbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.5 3-6 7-6s7 2.5 7 6"/></svg>
<span class="kole-m-tabbar__label">我的</span>
</button>
</nav>
</div>
</section>
<section class="demo-block" data-demo="count-5-dot">
<p class="demo-label">五项 + 红点(count=5 / badge=dot)</p>
<div class="demo-frame" data-assert="tabbar-count-5-badge-dot">
<div class="demo-frame__body">五项时标签文字仍不换行,宽度等分。</div>
<nav class="kole-m-tabbar" role="tablist" aria-label="五项导航">
<button class="kole-m-tabbar__item is-active" type="button" role="tab" aria-selected="true"><span class="kole-m-tabbar__label">首页</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false"><span class="kole-m-tabbar__label">分类</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false">
<span class="kole-m-tabbar__label">消息</span>
<span class="kole-m-tabbar__badge is-dot" aria-label="有新消息"></span>
</button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false"><span class="kole-m-tabbar__label">购物车</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false"><span class="kole-m-tabbar__label">我的</span></button>
</nav>
</div>
</section>
<section class="demo-block" data-demo="disabled">
<p class="demo-label">含禁用项(disabled 置灰且不响应)</p>
<div class="demo-frame" data-assert="tabbar-disabled">
<div class="demo-frame__body">禁用项保留位置,不参与切换。</div>
<nav class="kole-m-tabbar" role="tablist" aria-label="含禁用项导航">
<button class="kole-m-tabbar__item is-active" type="button" role="tab" aria-selected="true"><span class="kole-m-tabbar__label">首页</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false" disabled aria-label="订单(无权限)"><span class="kole-m-tabbar__label">订单</span></button>
<button class="kole-m-tabbar__item" type="button" role="tab" aria-selected="false"><span class="kole-m-tabbar__label">我的</span></button>
</nav>
</div>
</div>
</section>
<script>
/* 标签切换:同步 is-active / aria-selected / 面板 hidden */
(function () {
var first = document.querySelector('.kole-m-tabbar[aria-label="主导航"]');
if (!first) return;
first.addEventListener('click', function (e) {
var item = e.target.closest ? e.target.closest('.kole-m-tabbar__item') : null;
if (!item || item.disabled) return;
var frames = first.parentElement;
frames.querySelectorAll('[data-panel]').forEach(function (p) { p.hidden = true; });
var active = item.getAttribute('data-panel');
var panel = frames.querySelector('[data-panel="' + active + '"]');
if (panel) panel.hidden = false;
first.querySelectorAll('.kole-m-tabbar__item').forEach(function (b) {
var on = b === item;
b.classList.toggle('is-active', on);
b.setAttribute('aria-selected', on ? 'true' : 'false');
});
});
})();
</script>
<script>
/* ?demo=<id> → 只显示该演示块:文档站为每个演示单独起一个 375×640 预览帧。
无参数时全部显示 —— 测试页与回归走无参数路径,行为不变。 */
(function () {
var id = new URLSearchParams(location.search).get('demo');
if (!id) return;
var blocks = Array.prototype.slice.call(document.querySelectorAll('.demo-block'));
var hit = false;
blocks.forEach(function (b) {
var on = b.getAttribute('data-demo') === id;
if (on) hit = true;
b.hidden = !on;
});
if (!hit) { blocks.forEach(function (b) { b.hidden = false; }); return; }
document.body.classList.add('demo-single');
/* 单块模式去掉 min-height:100vh —— 否则内容高度随帧高变化(帧高→vh→内容高)形成反馈环,
自适应量到的永远是视口高度而不是内容高度。 */
var demoBox = document.querySelector('.demo');
if (demoBox) demoBox.style.minHeight = 'auto';
/* 单块模式:帧内不再重复显示演示标题行(文档页已给出标题与说明) */
blocks.forEach(function (b) {
var label = b.querySelector('.demo-label');
if (label && !b.hidden) label.hidden = true;
});
})();
</script>
</body>
</html>
frameworks-mobile/TabBar.jsx · React · 35 行
import React from 'react';
import './TabBar.css';
/* 底部标签栏(移动端)— 规格 §2;每项等分宽度、热区 ≥ 44px,选中态同步 aria-selected */
export default function TabBar({ items = [], value = '', onChange, label = '主导航' }) {
return (
<nav className="kole-m-tabbar" role="tablist" aria-label={label}>
{items.map((it) => {
const active = it.key === value;
return (
<button
key={it.key}
className={'kole-m-tabbar__item' + (active ? ' is-active' : '')}
type="button"
role="tab"
aria-selected={active ? 'true' : 'false'}
aria-label={it.ariaLabel}
disabled={!!it.disabled}
onClick={() => onChange && onChange(it.key)}
>
{it.icon ? (
<span className="kole-m-tabbar__icon" aria-hidden="true">
{it.icon}
</span>
) : null}
<span className="kole-m-tabbar__label">{it.label}</span>
{it.badge ? <span className="kole-m-tabbar__badge">{it.badge}</span> : null}
{it.dot ? <span className="kole-m-tabbar__badge is-dot" aria-label="有新消息" /> : null}
</button>
);
})}
</nav>
);
}
frameworks-mobile/TabBar.vue2.vue · Vue 2 · 35 行
<template>
<nav class="kole-m-tabbar" role="tablist" :aria-label="label">
<button
v-for="it in items"
:key="it.key"
class="kole-m-tabbar__item"
:class="{ 'is-active': it.key === value }"
type="button"
role="tab"
:aria-selected="it.key === value ? 'true' : 'false'"
:aria-label="it.ariaLabel"
:disabled="!!it.disabled"
@click="$emit('change', it.key)"
>
<span v-if="it.icon" class="kole-m-tabbar__icon" aria-hidden="true" v-html="it.icon"></span>
<span class="kole-m-tabbar__label">{{ it.label }}</span>
<span v-if="it.badge" class="kole-m-tabbar__badge">{{ it.badge }}</span>
<span v-if="it.dot" class="kole-m-tabbar__badge is-dot" aria-label="有新消息"></span>
</button>
</nav>
</template>
<script>
export default {
name: 'KoleMTabBar',
props: {
items: { type: Array, default: function () { return []; } },
value: { type: String, default: '' },
label: { type: String, default: '主导航' }
}
};
</script>
<style src="./TabBar.css"></style>
frameworks-mobile/TabBar.vue3.vue · Vue 3 · 33 行
<template>
<nav class="kole-m-tabbar" role="tablist" :aria-label="label">
<button
v-for="it in items"
:key="it.key"
class="kole-m-tabbar__item"
:class="{ 'is-active': it.key === value }"
type="button"
role="tab"
:aria-selected="it.key === value ? 'true' : 'false'"
:aria-label="it.ariaLabel"
:disabled="!!it.disabled"
@click="emit('change', it.key)"
>
<span v-if="it.icon" class="kole-m-tabbar__icon" aria-hidden="true" v-html="it.icon"></span>
<span class="kole-m-tabbar__label">{{ it.label }}</span>
<span v-if="it.badge" class="kole-m-tabbar__badge">{{ it.badge }}</span>
<span v-if="it.dot" class="kole-m-tabbar__badge is-dot" aria-label="有新消息"></span>
</button>
</nav>
</template>
<script setup>
const props = defineProps({
items: { type: Array, default: () => [] },
value: { type: String, default: '' },
label: { type: String, default: '主导航' }
});
const emit = defineEmits(['change']);
</script>
<style src="./TabBar.css"></style>
frameworks-mobile/TabBar.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 100 行
<template>
<view class="kole-m-tabbar">
<view
v-for="it in items"
:key="it.key"
class="kole-m-tabbar__item"
:class="it.key === value ? 'is-active' : ''"
role="button"
:aria-label="it.ariaLabel || it.label"
:aria-disabled="it.disabled ? 'true' : 'false'"
@tap="onTap(it)"
>
<view v-if="it.badge" class="kole-m-tabbar__badge">{{ it.badge }}</view>
<view v-else-if="it.dot" class="kole-m-tabbar__badge is-dot"></view>
<text class="kole-m-tabbar__label">{{ it.label }}</text>
</view>
</view>
</template>
<script setup>
/* uni-app 端 · 底部标签栏(移动端)— 规格 §2
跨端差异:图标交由宿主以 slot/图片提供(小程序的组件实例内不做 v-html 渲染),
选中态与禁用态走 is-active / aria-disabled,触控用 @tap。 */
const props = defineProps({
items: { type: Array, default: () => [] },
value: { type: String, default: '' },
label: { type: String, default: '主导航' }
});
const emit = defineEmits(['change']);
function onTap(it) {
if (it.disabled) return;
emit('change', it.key);
}
</script>
<style>
.kole-m-tabbar {
--kole-m-tabbar-height: 100rpx;
--kole-m-touch-target: 88rpx;
--kole-m-font-size-caption: 22rpx;
display: flex;
/* 底部安全区(全面屏横条) */
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
height: calc(var(--kole-m-tabbar-height) + constant(safe-area-inset-bottom));
height: calc(var(--kole-m-tabbar-height) + env(safe-area-inset-bottom));
box-sizing: content-box;
background-color: var(--kole-color-card-bg);
border-top: 1rpx solid var(--kole-color-border);
}
.kole-m-tabbar__item {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: var(--kole-m-touch-target);
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-caption);
}
.kole-m-tabbar__item.is-active { color: var(--kole-color-brand); }
.kole-m-tabbar__item[aria-disabled="true"] { color: var(--kole-color-text-disabled); }
.kole-m-tabbar__label {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 1.2;
}
.kole-m-tabbar__badge {
position: absolute;
top: 4rpx;
left: 50%;
margin-left: 4rpx;
min-width: 32rpx;
height: 32rpx;
padding: 0 8rpx;
border-radius: 16rpx;
background-color: var(--kole-color-error);
color: var(--kole-color-text-inverse);
font-size: 20rpx;
line-height: 32rpx;
text-align: center;
}
.kole-m-tabbar__badge.is-dot {
min-width: 16rpx;
width: 16rpx;
height: 16rpx;
padding: 0;
border-radius: 50%;
}
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 15 条 · 全部通过 报告 2026-09-20 16:50:33
node site/dev-server.js &
REG_BASE=http://127.0.0.1:3311 node tools/run-mobile-regression.mjs # 全量 5 个组件
npm run verify:mobile-docs # 本页内容完整性 + API 与源码一致性
设计契约
components/tabbar.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "2 · 底部标签栏 TabBar",
"confidence": "high",
"slug": "tabbar",
"name": "底部标签栏 TabBar",
"semanticTypeCandidates": [
"tabbar",
"bottom-navigation",
"tablist"
],
"variantDimensions": [
{
"name": "count",
"values": [
"2",
"3",
"4",
"5"
]
},
{
"name": "badge",
"values": [
"none",
"dot",
"number"
]
}
],
"representativeVariants": [
{
"count": "3",
"badge": "none",
"label": "三项基础"
},
{
"count": "4",
"badge": "number",
"label": "四项 + 数字角标"
},
{
"count": "5",
"badge": "dot",
"label": "五项 + 红点"
},
{
"count": "4",
"badge": "dot",
"label": "含禁用项"
}
],
"anatomy": {
"bar": "标签栏容器,固定于页面底部,高度 50px + 底部安全区",
"item": "单个标签,含图标与文字",
"icon": "图标,可为内联 SVG",
"label": "标签文字,11px",
"badge": "角标,可为数字或红点"
},
"structurePatterns": {
"count": "2 / 3 / 4 / 5(标签数量,超过 5 项应改用其它导航形态)",
"badge": "none / dot / number"
},
"usageHints": [
"底部主导航,2~5 个标签页之间切换,是移动端一级导航",
"固定于页面底部,高度 50px + 底部安全区",
"每项点击热区等分整栏宽度,高度不小于 44px",
"选中项切换后 aria-selected 同步变化",
"底部内边距包含安全区",
"标签文字 11px,图标可为内联 SVG"
],
"doNotInvent": [
"标签项超过 5 个时的滚动或折叠规则",
"图标资源的成套规则(规格只约定尺寸与语义)"
],
"unknowns": [
"角标超过两位数的收敛(如 99+)",
"选中态是否带图标填充切换"
],
"interaction": [
"每项点击热区等分整栏宽度,高度不小于 44px",
"选中项切换后 aria-selected 同步变化",
"底部内边距包含安全区"
],
"accessibility": [
"容器 role=tablist,单项 role=tab",
"选中项 aria-selected=true,未选中 false",
"每项具备 aria-label(图标 + 文字时可用文字代替)"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"props": [
{
"name": "items",
"type": "Item[]",
"default": "[]",
"desc": "标签项:{ key, label, icon?, badge?, dot?, disabled?, ariaLabel? }(规格 §2.2)",
"required": false
},
{
"name": "value",
"type": "string",
"default": "''",
"desc": "当前选中项的 key(受控,规格 §2.4)",
"required": false
},
{
"name": "label",
"type": "string",
"default": "'主导航'",
"desc": "整栏的无障碍名称(role=tablist 的 aria-label,规格 §2.6)",
"required": false
}
],
"events": [
{
"name": "change",
"params": "key: string",
"desc": "切换标签时触发,参数为选中项的 key"
}
],
"slots": [
{
"name": "—",
"desc": "无插槽;项由 items 驱动(uni-app 端的图标由宿主以图片/slot 提供,规格 §2.7)"
}
],
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。"
},
"variantClasses": {
"count": {
"2": [
"(.kole-m-tabbar__item)"
],
"3": [
"(.kole-m-tabbar__item)"
],
"4": [
"(.kole-m-tabbar__item)"
],
"5": [
"(.kole-m-tabbar__item)"
]
},
"badge": {
"none": [],
"dot": [
".kole-m-tabbar__badge",
".is-dot"
],
"number": [
".kole-m-tabbar__badge"
]
}
},
"demos": [
{
"id": "count-3",
"group": "01 组件类型",
"title": "三项",
"desc": "最常见的三项导航:图标 + 文字等分整栏宽度,热区高度 ≥ 44px。",
"variant": "count=3"
},
{
"id": "count-4-badge",
"group": "01 组件类型",
"title": "四项 + 数字角标",
"desc": "角标宽度随位数增长;超过两位数按业务约定收敛(如 99+)。",
"variant": "count=4 / badge=number"
},
{
"id": "count-5-dot",
"group": "01 组件类型",
"title": "五项 + 红点",
"desc": "五项是上限,标签文字仍不换行;红点用于「有更新但不必计数」。",
"variant": "count=5 / badge=dot"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "含禁用项",
"desc": "禁用项置灰、保留位置、不参与切换(无权限模块的常见表达)。",
"variant": "disabled"
}
],
"related": [
{
"slug": "navbar",
"why": "顶栏给标题与返回,标签栏给主模块切换;两者一起构成一级框架"
},
{
"slug": "pullrefresh",
"why": "都在页面上做纵向处理,注意手势与底部导航栏不要抢事件"
}
]
}