移动端导航底部标签栏

底部标签栏TabBar

底部主导航,2~5 个标签页之间切换,是移动端一级导航

导航 规格 2 · 底部标签栏 TabBar 6 端实现 触摸优先

引入(H5 原生;其余 5 端见「快速开始」)
<!-- ① 令牌: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 组件类型

三项count=3

最常见的三项导航:图标 + 文字等分整栏宽度,热区高度 ≥ 44px。

查看代码(演示页原文 · 24 行)
frameworks-mobile/TabBar.html · count-3
<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>
四项 + 数字角标count=4 / badge=number

角标宽度随位数增长;超过两位数按业务约定收敛(如 99+)。

查看代码(演示页原文 · 26 行)
frameworks-mobile/TabBar.html · count-4-badge
<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>
五项 + 红点count=5 / badge=dot

五项是上限,标签文字仍不换行;红点用于「有更新但不必计数」。

查看代码(演示页原文 · 16 行)
frameworks-mobile/TabBar.html · count-5-dot
<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 组件状态

含禁用项disabled

禁用项置灰、保留位置、不参与切换(无权限模块的常见表达)。

查看代码(演示页原文 · 12 行)
frameworks-mobile/TabBar.html · disabled
<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

名称类型默认值说明必传
itemsItem[][]标签项:{ key, label, icon?, badge?, dot?, disabled?, ariaLabel? }(规格 §2.2)N
valuestring''当前选中项的 key(受控,规格 §2.4)N
labelstring'主导航'整栏的无障碍名称(role=tablist 的 aria-label,规格 §2.6)N

「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。

事件

名称参数说明
changekey: 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 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
count2 / 3 / 4 / 5
2 (.kole-m-tabbar__item)
3 (.kole-m-tabbar__item)
4 (.kole-m-tabbar__item)
5 (.kole-m-tabbar__item)
badgenone / 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 令牌(改一处两端生效)。

--kole-m-font-size-caption --kole-m-safe-bottom --kole-m-tabbar-height --kole-m-touch-target --kole-color-border --kole-color-brand --kole-color-brand-bg --kole-color-card-bg --kole-color-error --kole-color-focus-ring --kole-color-text-disabled --kole-color-text-inverse --kole-color-text-secondary --kole-font-family --kole-font-family-num --kole-icon-size-20 --kole-space-4

6 端源码

同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。

frameworks-mobile/TabBar.css · 纯样式(CSS) · 97 行
frameworks-mobile/TabBar.css
/* 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 行
frameworks-mobile/TabBar.html
<!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 行
frameworks-mobile/TabBar.jsx
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 行
frameworks-mobile/TabBar.vue2.vue
<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 行
frameworks-mobile/TabBar.vue3.vue
<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 行
frameworks-mobile/TabBar.uniapp.vue
<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": "都在页面上做纵向处理,注意手势与底部导航栏不要抢事件"
    }
  ]
}