移动端导航按钮

按钮Button

触发一个即时动作

通用 规格 6 · 按钮 Button 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/mobile-button.css">

<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->

演示

每个演示都是真实渲染:预览帧加载 frameworks-mobile/Button.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。

01 组件类型

主按钮type=primary / size=large

一个区域里只放一个主按钮;移动端默认用 large 档(44px 高)。

查看代码(演示页原文 · 6 行)
frameworks-mobile/Button.html · primary
<section class="demo-block" data-demo="primary">
  <p class="demo-label">主按钮(type=primary / size=large)</p>
  <div class="demo-box">
    <button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" data-assert="btn-primary">提交订单</button>
  </div>
</section>
次按钮type=default

白底描边,与主按钮并排时表示次要动作。

查看代码(演示页原文 · 6 行)
frameworks-mobile/Button.html · default
<section class="demo-block" data-demo="default">
  <p class="demo-label">次按钮(type=default,与主按钮并排表示次要动作)</p>
  <div class="demo-box">
    <button class="kole-m-btn kole-m-btn--default" type="button" data-assert="btn-default">取消</button>
  </div>
</section>
文字按钮type=text

无背景无边框,用于弱操作(如「跳过」「查看详情」)。

查看代码(演示页原文 · 6 行)
frameworks-mobile/Button.html · text
<section class="demo-block" data-demo="text">
  <p class="demo-label">文字按钮(type=text,弱操作)</p>
  <div class="demo-box">
    <button class="kole-m-btn kole-m-btn--text" type="button" data-assert="btn-text">跳过这一步</button>
  </div>
</section>
危险按钮type=danger

删除类动作用错误色;不要和主按钮同屏并列出现两次。

查看代码(演示页原文 · 6 行)
frameworks-mobile/Button.html · danger
<section class="demo-block" data-demo="danger">
  <p class="demo-label">危险按钮(type=danger,删除类动作)</p>
  <div class="demo-box">
    <button class="kole-m-btn kole-m-btn--danger kole-m-btn--large" type="button" data-assert="btn-danger">删除订单</button>
  </div>
</section>

02 组件状态

三档尺寸size=small | default | large

large 44px 给主操作区,default 36px 给表单,small 28px 只用于行列内联。

查看代码(演示页原文 · 8 行)
frameworks-mobile/Button.html · sizes
<section class="demo-block" data-demo="sizes">
  <p class="demo-label">三档尺寸(small 28 / default 36 / large 44)</p>
  <div class="demo-box">
    <button class="kole-m-btn kole-m-btn--primary kole-m-btn--small" type="button" data-assert="btn-size-small">小</button>
    <button class="kole-m-btn kole-m-btn--primary" type="button" data-assert="btn-size-default">默认</button>
    <button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" data-assert="btn-size-large">大</button>
  </div>
</section>
禁用disabled=true

置灰且不响应;禁用原因要写在按钮附近,别只靠置灰暗示。

查看代码(演示页原文 · 6 行)
frameworks-mobile/Button.html · disabled
<section class="demo-block" data-demo="disabled">
  <p class="demo-label">禁用(disabled:置灰且不响应;禁用原因写在按钮附近)</p>
  <div class="demo-box">
    <button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" disabled data-assert="btn-disabled" aria-label="提交(库存不足,暂不可用)">库存不足</button>
  </div>
</section>
加载中loading=true

显示指示器并置 aria-busy,重复点击不再触发(点按钮切换)。

查看代码(演示页原文 · 9 行)
frameworks-mobile/Button.html · loading
<section class="demo-block" data-demo="loading">
  <p class="demo-label">加载中(loading:置 aria-busy,重复点击不再触发;点按钮切换)</p>
  <div class="demo-box">
    <button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" id="btn-loading"
            data-assert="btn-loading" data-behavior="click-toggles-class:#btn-loading|is-loading" aria-busy="false">
      <span class="kole-m-btn__label">提交</span>
    </button>
  </div>
</section>

API

props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs

Props

名称类型默认值说明必传
type'primary' | 'default' | 'text' | 'danger''default'变体 type:主次层级靠颜色与边框区分(规格 §6.3)N
size'large' | 'default' | 'small''default'变体 size;large 为 44px 移动端默认档(规格 §6.3)N
blockbooleanfalse块级形态,撑满容器宽度(规格 §6.2)N
disabledbooleanfalse置灰且不响应点击(规格 §6.4)N
loadingbooleanfalse显示加载指示器并阻止重复触发(规格 §6.4)N

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

事件

名称参数说明
click—点击时触发(disabled / loading 时不触发)

插槽

名称说明
default按钮文字(规格 §6.2)
icon可选图标或加载指示器,与文字间距 4px(规格 §6.2)

CSS 变量

组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。

名称默认值说明
本组件没有组件级 CSS 变量

何时使用

  • 触发一个即时动作
  • 高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作
  • 按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)
  • loading 期间重复点击不触发第二次事件
  • 仅有图标时必须给 aria-label

交互与触控

  • 高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作
  • 按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)
  • loading 期间重复点击不触发第二次事件

无障碍

  • 使用原生 button,天然可聚焦、可键盘触发
  • 仅有图标时必须给 aria-label
  • loading 时置 aria-busy="true" 且 disabled(避免重复提交)

从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。

组件何时用它而不是本组件
动作面板ActionSheet需要从一个入口提供多个操作时用动作面板;单个即时动作才用按钮
单元格Cell整行进入下级用单元格;单个动作才用按钮

规格未定 / 禁止发明

类别条目
禁止发明按钮内多行文字的排版规则
禁止发明长按(long-press)的附加行为
规格未定图标与文字同时存在时的最小宽度
规格未定危险按钮是否需要二次确认

结构(anatomy)

字段说明
button根元素,用原生 button,圆角取令牌
label按钮文字,单行不换行
icon可选图标或加载指示器,与文字间距 4px
block可选块级形态,撑满容器宽度

变体维度与类名映射

类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
typeprimary / default / text / danger
primary .kole-m-btn--primary
default .kole-m-btn--default
text .kole-m-btn--text
danger .kole-m-btn--danger
sizelarge / default / small
large .kole-m-btn--large
default (由数据驱动,无专属类)
small .kole-m-btn--small
blockfalse / true
false (由数据驱动,无专属类)
true .is-block

代表变体

变体标签
type=primary · size=large主按钮(移动端默认档)
type=default · size=default次按钮
type=text · size=default文字按钮
type=danger · size=large危险按钮

用到的令牌

构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。

--kole-m-font-size-body --kole-m-font-size-caption --kole-m-font-size-label --kole-m-touch-target --kole-color-border --kole-color-brand --kole-color-brand-active --kole-color-brand-bg --kole-color-card-bg --kole-color-disabled-bg --kole-color-error --kole-color-focus-ring --kole-color-text-body --kole-color-text-disabled --kole-color-text-inverse --kole-duration-fast --kole-ease-standard --kole-radius-base --kole-space-12 --kole-space-16 --kole-space-20 --kole-space-4

6 端源码

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

frameworks-mobile/Button.css · 纯样式(CSS) · 92 行
frameworks-mobile/Button.css
/* Kole UI Mobile · Button 样式 — 对齐 .design_library/kole-ui-mobile/spec/移动端规格.md §6
   按钮:高度分三档(44 / 36 / 28),按下反馈用 :active 背景(移动端没有 hover)。
   令牌:颜色/字体/圆角取 --kole-*,触控尺寸取 --kole-m-*。 */

.kole-m-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--kole-space-4);
  box-sizing: border-box;
  min-height: 36px;
  padding: 0 var(--kole-space-16);
  border: 1px solid transparent;
  border-radius: var(--kole-radius-base);
  background: transparent;
  color: var(--kole-color-text-body);
  font-family: inherit;
  font-size: var(--kole-m-font-size-label);
  line-height: 1.2;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  transition: background-color var(--kole-duration-fast) var(--kole-ease-standard),
              border-color var(--kole-duration-fast) var(--kole-ease-standard),
              color var(--kole-duration-fast) var(--kole-ease-standard);
}

/* 变体 size:large = 移动端主操作区(44px 触控最小边长) */
.kole-m-btn--large {
  min-height: var(--kole-m-touch-target);
  padding: 0 var(--kole-space-20);
  font-size: var(--kole-m-font-size-body);
}
.kole-m-btn--small {
  min-height: 28px;
  padding: 0 var(--kole-space-12);
  font-size: var(--kole-m-font-size-caption);
}

/* 变体 type */
.kole-m-btn--primary { background: var(--kole-color-brand); color: var(--kole-color-text-inverse); }
.kole-m-btn--primary:active { background: var(--kole-color-brand-active); }

.kole-m-btn--default { background: var(--kole-color-card-bg); border-color: var(--kole-color-border); }
.kole-m-btn--default:active { border-color: var(--kole-color-brand); color: var(--kole-color-brand); }

.kole-m-btn--text { color: var(--kole-color-brand); }
.kole-m-btn--text:active { background: var(--kole-color-brand-bg); }

.kole-m-btn--danger { background: var(--kole-color-error); color: var(--kole-color-text-inverse); }
.kole-m-btn--danger:active { filter: brightness(.92); }

/* 变体 block:撑满容器 */
.kole-m-btn.is-block { display: flex; width: 100%; }

/* 状态:禁用 / 加载中 */
.kole-m-btn:disabled,
.kole-m-btn.is-disabled {
  background: var(--kole-color-disabled-bg);
  border-color: transparent;
  color: var(--kole-color-text-disabled);
  cursor: not-allowed;
}
.kole-m-btn.is-loading { cursor: progress; }

.kole-m-btn__icon { display: inline-flex; flex: 0 0 auto; }
.kole-m-btn__label { white-space: nowrap; }

.kole-m-btn__spinner {
  width: 14px;
  height: 14px;
  box-sizing: border-box;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: kole-m-btn-spin .6s linear infinite;
}

@keyframes kole-m-btn-spin {
  to { transform: rotate(360deg); }
}

.kole-m-btn:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .kole-m-btn__spinner { animation: none; }
}
frameworks-mobile/Button.html · H5 原生(无框架) · 122 行
frameworks-mobile/Button.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 · Button(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Button.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); }
  .demo-box { display: flex; flex-wrap: wrap; align-items: center; gap: var(--kole-space-12);
    padding: var(--kole-m-gutter); background: var(--kole-color-card-bg);
    border-block: 1px solid var(--kole-color-border); }
  .demo-box--block { display: block; }
  .demo-block[hidden] { display: none; }
  body.demo-single .demo-label:first-child { padding-top: var(--kole-space-12); }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="primary">
    <p class="demo-label">主按钮(type=primary / size=large)</p>
    <div class="demo-box">
      <button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" data-assert="btn-primary">提交订单</button>
    </div>
  </section>

  <section class="demo-block" data-demo="default">
    <p class="demo-label">次按钮(type=default,与主按钮并排表示次要动作)</p>
    <div class="demo-box">
      <button class="kole-m-btn kole-m-btn--default" type="button" data-assert="btn-default">取消</button>
    </div>
  </section>

  <section class="demo-block" data-demo="text">
    <p class="demo-label">文字按钮(type=text,弱操作)</p>
    <div class="demo-box">
      <button class="kole-m-btn kole-m-btn--text" type="button" data-assert="btn-text">跳过这一步</button>
    </div>
  </section>

  <section class="demo-block" data-demo="danger">
    <p class="demo-label">危险按钮(type=danger,删除类动作)</p>
    <div class="demo-box">
      <button class="kole-m-btn kole-m-btn--danger kole-m-btn--large" type="button" data-assert="btn-danger">删除订单</button>
    </div>
  </section>

  <section class="demo-block" data-demo="sizes">
    <p class="demo-label">三档尺寸(small 28 / default 36 / large 44)</p>
    <div class="demo-box">
      <button class="kole-m-btn kole-m-btn--primary kole-m-btn--small" type="button" data-assert="btn-size-small">小</button>
      <button class="kole-m-btn kole-m-btn--primary" type="button" data-assert="btn-size-default">默认</button>
      <button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" data-assert="btn-size-large">大</button>
    </div>
  </section>

  <section class="demo-block" data-demo="disabled">
    <p class="demo-label">禁用(disabled:置灰且不响应;禁用原因写在按钮附近)</p>
    <div class="demo-box">
      <button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" disabled data-assert="btn-disabled" aria-label="提交(库存不足,暂不可用)">库存不足</button>
    </div>
  </section>

  <section class="demo-block" data-demo="loading">
    <p class="demo-label">加载中(loading:置 aria-busy,重复点击不再触发;点按钮切换)</p>
    <div class="demo-box">
      <button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" id="btn-loading"
              data-assert="btn-loading" data-behavior="click-toggles-class:#btn-loading|is-loading" aria-busy="false">
        <span class="kole-m-btn__label">提交</span>
      </button>
    </div>
  </section>
</div>
<script>
  /* 演示页脚本:loading 态切换(真实业务里由请求的 pending 状态驱动) */
  (function () {
    var btn = document.getElementById('btn-loading');
    if (!btn) return;
    var label = btn.querySelector('.kole-m-btn__label');
    btn.addEventListener('click', function () {
      if (btn.disabled) return;
      var on = btn.classList.toggle('is-loading');
      btn.setAttribute('aria-busy', on ? 'true' : 'false');
      btn.disabled = on;                       /* 规格 §6.4:loading 期间阻止重复触发 */
      if (on) {
        btn.insertAdjacentHTML('afterbegin', '<span class="kole-m-btn__spinner" aria-hidden="true"></span>');
        label.textContent = '提交中…';
      } else {
        var sp = btn.querySelector('.kole-m-btn__spinner');
        if (sp) sp.remove();
        label.textContent = '提交';
      }
    });
  })();
</script>
<script>
  /* ?demo=<id> → 只显示该演示块(文档站按块预览用;无参数时全部显示,测试与回归走无参数路径) */
  (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');
    blocks.forEach(function (b) {
      var label = b.querySelector('.demo-label');
      if (label && !b.hidden) label.hidden = true;
    });
  })();
</script>
</body>
</html>
frameworks-mobile/Button.jsx · React · 42 行
frameworks-mobile/Button.jsx
import React from 'react';
import './Button.css';

/* 按钮(移动端)— 规格 §6;三档高度(44 / 36 / 28),loading 期间阻止重复触发 */
export default function Button({
  type = 'default',
  size = 'default',
  block = false,
  disabled = false,
  loading = false,
  onClick,
  children,
  icon = null,
}) {
  const cls =
    'kole-m-btn' +
    (type === 'primary' ? ' kole-m-btn--primary' : '') +
    (type === 'text' ? ' kole-m-btn--text' : '') +
    (type === 'danger' ? ' kole-m-btn--danger' : '') +
    (type === 'default' ? ' kole-m-btn--default' : '') +
    (size === 'large' ? ' kole-m-btn--large' : '') +
    (size === 'small' ? ' kole-m-btn--small' : '') +
    (block ? ' is-block' : '') +
    (loading ? ' is-loading' : '');

  return (
    <button
      className={cls}
      type="button"
      disabled={disabled || loading}
      aria-busy={loading ? 'true' : 'false'}
      onClick={() => {
        if (disabled || loading) return;
        if (onClick) onClick();
      }}
    >
      {loading ? <span className="kole-m-btn__spinner" aria-hidden="true" /> : icon}
      <span className="kole-m-btn__label">{children}</span>
    </button>
  );
}
frameworks-mobile/Button.vue2.vue · Vue 2 · 43 行
frameworks-mobile/Button.vue2.vue
<template>
  <button
    class="kole-m-btn"
    :class="btnClass"
    type="button"
    :disabled="disabled || loading"
    :aria-busy="loading ? 'true' : 'false'"
    @click="$emit('click')"
  >
    <span v-if="loading" class="kole-m-btn__spinner" aria-hidden="true"></span>
    <span v-else-if="$slots.icon" class="kole-m-btn__icon"><slot name="icon"></slot></span>
    <span class="kole-m-btn__label"><slot>按钮</slot></span>
  </button>
</template>

<script>
var TYPE_CLASS = { primary: 'kole-m-btn--primary', default: 'kole-m-btn--default', text: 'kole-m-btn--text', danger: 'kole-m-btn--danger' };
var SIZE_CLASS = { large: 'kole-m-btn--large', small: 'kole-m-btn--small' };

export default {
  name: 'KoleMButton',
  props: {
    type: { type: String, default: 'default' },
    size: { type: String, default: 'default' },
    block: { type: Boolean, default: false },
    disabled: { type: Boolean, default: false },
    loading: { type: Boolean, default: false }
  },
  computed: {
    btnClass: function () {
      return [
        TYPE_CLASS[this.type],
        SIZE_CLASS[this.size],
        this.block ? 'is-block' : '',
        this.loading ? 'is-loading' : ''
      ].filter(Boolean);
    }
  }
};
</script>

<style src="./Button.css"></style>
frameworks-mobile/Button.vue3.vue · Vue 3 · 37 行
frameworks-mobile/Button.vue3.vue
<template>
  <button
    class="kole-m-btn"
    :class="btnClass"
    type="button"
    :disabled="disabled || loading"
    :aria-busy="loading ? 'true' : 'false'"
    @click="emit('click')"
  >
    <span v-if="loading" class="kole-m-btn__spinner" aria-hidden="true"></span>
    <span v-else-if="$slots.icon" class="kole-m-btn__icon"><slot name="icon" /></span>
    <span class="kole-m-btn__label"><slot>按钮</slot></span>
  </button>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  type: { type: String, default: 'default' },
  size: { type: String, default: 'default' },
  block: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false },
  loading: { type: Boolean, default: false }
});
const emit = defineEmits(['click']);

/* 与 React 端同一套类名组合规则 */
const TYPE_CLASS = { primary: 'kole-m-btn--primary', default: 'kole-m-btn--default', text: 'kole-m-btn--text', danger: 'kole-m-btn--danger' };
const SIZE_CLASS = { large: 'kole-m-btn--large', small: 'kole-m-btn--small' };
const btnClass = computed(() =>
  [TYPE_CLASS[props.type], SIZE_CLASS[props.size], props.block ? 'is-block' : '', props.loading ? 'is-loading' : ''].filter(Boolean)
);
</script>

<style src="./Button.css"></style>
frameworks-mobile/Button.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 95 行
frameworks-mobile/Button.uniapp.vue
<template>
  <view
    class="kole-m-btn"
    :class="btnClass"
    role="button"
    :aria-disabled="disabled || loading ? 'true' : 'false'"
    :aria-busy="loading ? 'true' : 'false'"
    @tap="onTap"
  >
    <view v-if="loading" class="kole-m-btn__spinner"></view>
    <text class="kole-m-btn__label"><slot></slot></text>
  </view>
</template>

<script setup>
/* uni-app 端 · 按钮(移动端)— 规格 §6
   跨端差异:用 uni 基础组件(view / text),点击用 @tap;尺寸用 rpx
   (750rpx = 视口宽度,88rpx = 375pt 下的 44px 触控最小边长)。 */
import { computed } from 'vue';

const TYPE_CLASS = { primary: 'kole-m-btn--primary', default: 'kole-m-btn--default', text: 'kole-m-btn--text', danger: 'kole-m-btn--danger' };
const SIZE_CLASS = { large: 'kole-m-btn--large', small: 'kole-m-btn--small' };

const props = defineProps({
  type: { type: String, default: 'default' },
  size: { type: String, default: 'default' },
  block: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false },
  loading: { type: Boolean, default: false }
});
const emit = defineEmits(['click']);

const btnClass = computed(() =>
  [TYPE_CLASS[props.type], SIZE_CLASS[props.size], props.block ? 'is-block' : '', props.loading ? 'is-loading' : ''].filter(Boolean)
);

function onTap() {
  /* 规格 §6.5:loading 期间重复点击不触发第二次事件 */
  if (props.disabled || props.loading) return;
  emit('click');
}
</script>

<style>
.kole-m-btn {
  --kole-m-touch-target: 88rpx;
  --kole-m-font-size-body: 32rpx;
  --kole-m-font-size-label: 28rpx;
  --kole-m-font-size-caption: 22rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: 72rpx;
  padding: 0 32rpx;
  border: 1rpx solid transparent;
  border-radius: var(--kole-radius-base);
  background-color: transparent;
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-label);
}

.kole-m-btn--large { min-height: var(--kole-m-touch-target); padding: 0 40rpx; font-size: var(--kole-m-font-size-body); }
.kole-m-btn--small { min-height: 56rpx; padding: 0 24rpx; font-size: var(--kole-m-font-size-caption); }

.kole-m-btn--primary { background-color: var(--kole-color-brand); color: var(--kole-color-text-inverse); }
.kole-m-btn--default { background-color: var(--kole-color-card-bg); border-color: var(--kole-color-border); }
.kole-m-btn--text { color: var(--kole-color-brand); }
.kole-m-btn--danger { background-color: var(--kole-color-error); color: var(--kole-color-text-inverse); }

.kole-m-btn.is-block { width: 100%; }

.kole-m-btn[aria-disabled="true"] {
  background-color: var(--kole-color-disabled-bg);
  border-color: transparent;
  color: var(--kole-color-text-disabled);
}

.kole-m-btn__label { line-height: 1.2; }

.kole-m-btn__spinner {
  width: 28rpx;
  height: 28rpx;
  box-sizing: border-box;
  border: 4rpx solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: kole-m-btn-spin 600ms linear infinite;
}

@keyframes kole-m-btn-spin {
  to { transform: rotate(360deg); }
}
</style>

测试与回归

断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。

断言 20 条 · 全部通过 报告 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/mobile-button.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "confidence": "high",
  "specSection": "6 · 按钮 Button",
  "slug": "mobile-button",
  "name": "按钮 Button",
  "semanticTypeCandidates": [
    "button",
    "action"
  ],
  "variantDimensions": [
    {
      "name": "type",
      "values": [
        "primary",
        "default",
        "text",
        "danger"
      ]
    },
    {
      "name": "size",
      "values": [
        "large",
        "default",
        "small"
      ]
    },
    {
      "name": "block",
      "values": [
        "false",
        "true"
      ]
    }
  ],
  "representativeVariants": [
    {
      "type": "primary",
      "size": "large",
      "label": "主按钮(移动端默认档)"
    },
    {
      "type": "default",
      "size": "default",
      "label": "次按钮"
    },
    {
      "type": "text",
      "size": "default",
      "label": "文字按钮"
    },
    {
      "type": "danger",
      "size": "large",
      "label": "危险按钮"
    }
  ],
  "anatomy": {
    "button": "根元素,用原生 button,圆角取令牌",
    "label": "按钮文字,单行不换行",
    "icon": "可选图标或加载指示器,与文字间距 4px",
    "block": "可选块级形态,撑满容器宽度"
  },
  "structurePatterns": {
    "type": "primary / default / text / danger",
    "size": "large(44px 高,移动端默认)/ default(36px)/ small(28px)"
  },
  "usageHints": [
    "触发一个即时动作",
    "高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作",
    "按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)",
    "loading 期间重复点击不触发第二次事件",
    "仅有图标时必须给 aria-label"
  ],
  "doNotInvent": [
    "按钮内多行文字的排版规则",
    "长按(long-press)的附加行为"
  ],
  "unknowns": [
    "图标与文字同时存在时的最小宽度",
    "危险按钮是否需要二次确认"
  ],
  "interaction": [
    "高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作",
    "按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)",
    "loading 期间重复点击不触发第二次事件"
  ],
  "accessibility": [
    "使用原生 button,天然可聚焦、可键盘触发",
    "仅有图标时必须给 aria-label",
    "loading 时置 aria-busy=\"true\" 且 disabled(避免重复提交)"
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "type",
        "type": "'primary' | 'default' | 'text' | 'danger'",
        "default": "'default'",
        "desc": "变体 type:主次层级靠颜色与边框区分(规格 §6.3)",
        "required": false
      },
      {
        "name": "size",
        "type": "'large' | 'default' | 'small'",
        "default": "'default'",
        "desc": "变体 size;large 为 44px 移动端默认档(规格 §6.3)",
        "required": false
      },
      {
        "name": "block",
        "type": "boolean",
        "default": "false",
        "desc": "块级形态,撑满容器宽度(规格 §6.2)",
        "required": false
      },
      {
        "name": "disabled",
        "type": "boolean",
        "default": "false",
        "desc": "置灰且不响应点击(规格 §6.4)",
        "required": false
      },
      {
        "name": "loading",
        "type": "boolean",
        "default": "false",
        "desc": "显示加载指示器并阻止重复触发(规格 §6.4)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "click",
        "params": "—",
        "desc": "点击时触发(disabled / loading 时不触发)"
      }
    ],
    "slots": [
      {
        "name": "default",
        "desc": "按钮文字(规格 §6.2)"
      },
      {
        "name": "icon",
        "desc": "可选图标或加载指示器,与文字间距 4px(规格 §6.2)"
      }
    ]
  },
  "variantClasses": {
    "type": {
      "primary": [
        ".kole-m-btn--primary"
      ],
      "default": [
        ".kole-m-btn--default"
      ],
      "text": [
        ".kole-m-btn--text"
      ],
      "danger": [
        ".kole-m-btn--danger"
      ]
    },
    "size": {
      "large": [
        ".kole-m-btn--large"
      ],
      "default": [],
      "small": [
        ".kole-m-btn--small"
      ]
    },
    "block": {
      "false": [],
      "true": [
        ".is-block"
      ]
    }
  },
  "demos": [
    {
      "id": "primary",
      "group": "01 组件类型",
      "title": "主按钮",
      "desc": "一个区域里只放一个主按钮;移动端默认用 large 档(44px 高)。",
      "variant": "type=primary / size=large"
    },
    {
      "id": "default",
      "group": "01 组件类型",
      "title": "次按钮",
      "desc": "白底描边,与主按钮并排时表示次要动作。",
      "variant": "type=default"
    },
    {
      "id": "text",
      "group": "01 组件类型",
      "title": "文字按钮",
      "desc": "无背景无边框,用于弱操作(如「跳过」「查看详情」)。",
      "variant": "type=text"
    },
    {
      "id": "danger",
      "group": "01 组件类型",
      "title": "危险按钮",
      "desc": "删除类动作用错误色;不要和主按钮同屏并列出现两次。",
      "variant": "type=danger"
    },
    {
      "id": "sizes",
      "group": "02 组件状态",
      "title": "三档尺寸",
      "desc": "large 44px 给主操作区,default 36px 给表单,small 28px 只用于行列内联。",
      "variant": "size=small | default | large"
    },
    {
      "id": "disabled",
      "group": "02 组件状态",
      "title": "禁用",
      "desc": "置灰且不响应;禁用原因要写在按钮附近,别只靠置灰暗示。",
      "variant": "disabled=true"
    },
    {
      "id": "loading",
      "group": "02 组件状态",
      "title": "加载中",
      "desc": "显示指示器并置 aria-busy,重复点击不再触发(点按钮切换)。",
      "variant": "loading=true"
    }
  ],
  "related": [
    {
      "slug": "actionsheet",
      "why": "需要从一个入口提供多个操作时用动作面板;单个即时动作才用按钮"
    },
    {
      "slug": "cell",
      "why": "整行进入下级用单元格;单个动作才用按钮"
    }
  ]
}