UI组件设计规范

360 浏览
36 试用
10 购买
Nov 24, 2025更新

本提示词可根据指定组件类型生成完整UI设计规范,包括视觉属性、交互状态、可访问性考虑及代码实现说明,确保文档结构清晰、专业可复用,适用于多产品线和前端开发团队参考使用。

1. 组件类型和设计优先级说明

  • 组件类型:主按钮(Primary Button),包含默认、悬停、按下(激活)、禁用、加载态。
  • 使用场景:页面或模块的首要行动(CTA),例如“提交”“保存”“继续”“购买”等。每个视图中应尽量只存在一个主按钮,避免视觉竞争。
  • 设计优先级:
    1. 视觉一致性:统一色彩、形状、阴影、排版与间距;在不同平台与主题模式(浅色/深色)保持一致体验。
    2. 可用性:清晰的层级、足够的触控面积、明确的状态反馈、避免布局跳动。
    3. 可访问性:对比度、键盘可达性、焦点可见、低动画偏好支持、语义与ARIA正确使用。
    4. 性能优化:使用 GPU 加速的变换、避免重排与重绘、尽量用 CSS 动画、减少 DOM 与状态切换开销。

2. 视觉属性(颜色、字体、间距、尺寸等)

  • 色彩(建议以设计令牌/变量管理,支持浅/深色主题)
    • 浅色主题
      • 背景(默认):--primary-600
      • 文本/图标:--on-primary(通常为白)
      • 悬停背景:--primary-700
      • 按下背景:--primary-800
      • 禁用背景:--primary-300
      • 禁用文本:--on-primary-disabled(白的 70% 不透明或灰)
      • 焦点环:--focus-ring(高可见度蓝/品牌色)
    • 深色主题
      • 背景(默认):--primary-500
      • 悬停背景:--primary-600
      • 按下背景:--primary-700
      • 文本/图标:--on-primary-dark(通常为白)
      • 禁用背景:--primary-400(降低饱和度/亮度)
      • 焦点环:--focus-ring-dark
  • 排版
    • 字体族:系统字体栈(-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif)
    • 字重:600(Semibold)
    • 字号:
      • S:14px
      • M:16px(默认)
      • L:16px(大尺寸按钮建议依靠内边距扩大触控面积)
    • 行高:1(按钮内文本垂直居中)
    • 大小写:句式(首字母大写或按语言规范),避免全大写
  • 尺寸与间距(触控最小目标≥44px)
    • 高度:
      • S:32px(桌面密度)
      • M:40px(默认)
      • L:48px(移动优先)
    • 水平内边距(左右):
      • S:12px
      • M:16px
      • L:20px
    • 最小宽度:64px;自适应文案长度,支持 text-overflow: ellipsis
    • 图标与文本间距:8px
    • 角半径:8px(可在多产品线中允许 6–12px 的可配置范围)
    • 阴影(桌面,提升层级)
      • 默认:微弱阴影(例如 0 1px 2px rgba(0,0,0,0.12))
      • 悬停:增强阴影(例如 0 2px 6px rgba(0,0,0,0.16))
      • 按下:减弱阴影(例如 0 1px 2px rgba(0,0,0,0.08))
  • 加载指示
    • Spinner 尺寸:16px(随按钮字号缩放,建议使用 em)
    • 位置:文本左侧或居中(推荐左侧,保留文本以减少无障碍困扰)
    • 动画:旋转 600ms/周;在低动画偏好下禁用或降速
  • 内容规范
    • 文案简短明确,首要动词+对象(例如“提交表单”)
    • 避免多行;超长时省略号处理并提供 tooltip(桌面)

3. 交互状态(默认、悬停、点击、禁用、加载态)

  • 默认(rest)
    • 背景:--primary-600(浅色主题)
    • 文本/图标:--on-primary
    • 阴影:弱
  • 悬停(hover,桌面)
    • 背景:--primary-700
    • 阴影:增强
    • 条件:仅在设备支持 hover 时启用(@media (hover: hover))
  • 聚焦(focus-visible,键盘)
    • 焦点环:2px 实线,outline-offset: 2px,颜色为 --focus-ring
    • 背景不变;确保与周围背景对比度≥3:1
  • 按下(active/pressed)
    • 背景:--primary-800
    • 阴影:减弱
    • 微动画:transform: scale(0.98) 或 translateY(1px)(在 reduced-motion 下禁用)
  • 禁用(disabled)
    • 背景:--primary-300(或降低不透明度)
    • 文本:--on-primary-disabled
    • 阴影:无;鼠标指针:not-allowed;不可聚焦
  • 加载(loading)
    • 状态:阻止重复点击;显示 spinner;文本保留或替换为“正在…”
    • 语义:aria-busy="true";按钮设置 disabled
    • 动效:spinner 旋转;按钮不位移(预留 spinner 空间防止布局跳动)

4. 可访问性考虑(辅助功能、可读性、对比度等)

  • 对比度
    • 主按钮背景与文本对比度≥4.5:1(WCAG AA)
    • 焦点环与周围背景≥3:1
    • 禁用态可降低对比度,但文本仍应可辨识
  • 键盘与屏幕阅读器
    • 使用原生
    • 焦点可见::focus-visible 不移除;严禁用 outline: none 无替代
    • 加载态:aria-busy="true";更新按钮内部的状态文本并设置 aria-live="polite" 以提醒用户“正在提交…”
    • 禁用态:使用 disabled 属性(原生不可聚焦);避免仅使用 aria-disabled
  • 动画与动效
    • 支持 @media (prefers-reduced-motion: reduce):禁用或降低动画速率,避免 scale/translate
  • 触控目标
    • 高度≥44px(移动);点击区域至少与视觉尺寸一致
  • 语言与国际化
    • 多语言文案长度可变;文本溢出使用省略号并提供备用提示(桌面)
  • 强制高对比模式(Windows)
    • @media (forced-colors: active):使用系统色 ButtonFace/ButtonText;保留焦点环 Highlight
  • 图标与可见性
    • 图标仅辅助;不可替代文本。加载 spinner 设置 aria-hidden="true",避免重复朗读

5. 代码实现说明(HTML/CSS/JS示例或组件代码规范)

示例采用 CSS 变量作为设计令牌,支持浅/深色主题与多产品线复用。

<!-- 主按钮:默认(可含图标与加载状态) -->
<button class="btn btn-primary btn--md" type="button" aria-live="polite">
  <span class="btn__spinner" aria-hidden="true"></span>
  <span class="btn__label">提交</span>
  <!-- 可选图标:放在文本左侧
  <svg class="btn__icon" aria-hidden="true" ...></svg>
  -->
  <span class="sr-only" data-status></span> <!-- 用于 aria-live 通知 -->
</button>
/* 设计令牌(示例,可根据品牌调优) */
:root {
  /* 浅色主题 */
  --primary-300: #A5D3FF;
  --primary-500: #2D8CFF;
  --primary-600: #1E7AF8;
  --primary-700: #136AE0;
  --primary-800: #0D58BE;
  --on-primary: #FFFFFF;
  --on-primary-disabled: rgba(255,255,255,0.7);

  --focus-ring: #1E7AF8; /* 或品牌辅助色 */
  --shadow-weak: 0 1px 2px rgba(0,0,0,0.12);
  --shadow-strong: 0 2px 6px rgba(0,0,0,0.16);

  --spinner-size: 1em; /* 随字号缩放 */
  --spinner-stroke: 2px;
}

/* 深色主题覆盖(示例) */
@media (prefers-color-scheme: dark) {
  :root {
    --primary-300: #5FA0E6;
    --primary-500: #2D8CFF;
    --primary-600: #1E7AF8;
    --primary-700: #136AE0;
    --primary-800: #0D58BE;
    --on-primary: #FFFFFF;
    --on-primary-disabled: rgba(255,255,255,0.65);
    --focus-ring: #9BC9FF;
  }
}

/* 基础按钮样式 */
.btn {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
  font-weight: 600;
  line-height: 1;
  border: none;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  text-decoration: none;
  transition: background-color 120ms ease, box-shadow 120ms ease, transform 60ms ease;
  will-change: background-color, box-shadow, transform;
}

/* 尺寸 */
.btn--sm { font-size: 14px; height: 32px; padding: 0 12px; }
.btn--md { font-size: 16px; height: 40px; padding: 0 16px; }
.btn--lg { font-size: 16px; height: 48px; padding: 0 20px; }

/* 主按钮变体 */
.btn-primary {
  background-color: var(--primary-600);
  color: var(--on-primary);
  box-shadow: var(--shadow-weak);
}

/* 悬停:仅在有 hover 能力设备启用 */
@media (hover: hover) {
  .btn-primary:hover:not(:disabled):not([aria-busy="true"]) {
    background-color: var(--primary-700);
    box-shadow: var(--shadow-strong);
  }
}

/* 聚焦可见 */
.btn-primary:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* 按下(激活) */
.btn-primary:active:not(:disabled):not([aria-busy="true"]) {
  background-color: var(--primary-800);
  box-shadow: var(--shadow-weak);
  transform: scale(0.98);
}

/* 禁用 */
.btn-primary:disabled {
  background-color: var(--primary-300);
  color: var(--on-primary-disabled);
  box-shadow: none;
  cursor: not-allowed;
}

/* 文本溢出处理 */
.btn__label {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 预留 spinner 空间,避免布局跳动 */
.btn__spinner {
  width: var(--spinner-size);
  height: var(--spinner-size);
  border-radius: 50%;
  border: var(--spinner-stroke) solid rgba(255,255,255,0.35);
  border-top-color: currentColor;
  display: inline-block;
  visibility: hidden; /* 默认不占位可改为 visibility 以保留空间 */
  opacity: 0;
  transition: opacity 120ms ease, visibility 0s linear 120ms;
}

/* 加载态样式:显示 spinner,禁用交互 */
.btn-primary[aria-busy="true"] {
  pointer-events: none;
}

.btn-primary[aria-busy="true"] .btn__spinner {
  visibility: visible;
  opacity: 1;
  animation: spin 600ms linear infinite;
  transition: opacity 120ms ease;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* 低动画偏好 */
@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
  .btn-primary:active { transform: none; }
  .btn__spinner { animation: none; }
}

/* 强制高对比模式兼容(Windows) */
@media (forced-colors: active) {
  .btn-primary {
    background: ButtonFace;
    color: ButtonText;
    box-shadow: none;
  }
  .btn-primary:focus-visible {
    outline: 2px solid Highlight;
  }
}
// 最小交互逻辑:加载态管理与无障碍提示
function setButtonLoading(btn, isLoading, statusText = '正在提交…') {
  if (!(btn instanceof HTMLButtonElement)) return;
  const statusNode = btn.querySelector('[data-status]');
  const spinner = btn.querySelector('.btn__spinner');

  if (isLoading) {
    btn.setAttribute('aria-busy', 'true');
    btn.disabled = true; // 防止重复点击
    if (statusNode) statusNode.textContent = statusText; // aria-live=polite 将提示
    if (spinner) spinner.style.visibility = 'visible'; // 如需强制保留空间可在 CSS 中始终可见但透明
  } else {
    btn.removeAttribute('aria-busy');
    btn.disabled = false;
    if (statusNode) statusNode.textContent = '';
    if (spinner) {
      spinner.style.visibility = ''; // 复位
    }
  }
}

// 使用示例
const submitBtn = document.querySelector('.btn-primary');
submitBtn?.addEventListener('click', async () => {
  setButtonLoading(submitBtn, true, '正在提交…');
  try {
    // 执行异步操作
    await new Promise(res => setTimeout(res, 1500));
    // 成功后可更新文案或状态
  } catch (e) {
    // 失败处理:提示错误
  } finally {
    setButtonLoading(submitBtn, false);
  }
});

代码规范与实现要点:

  • 语义元素:使用原生
  • 禁用:用 disabled 属性(原生不可聚焦与不可点击),避免仅 aria-disabled。
  • 加载态:
    • 设置 aria-busy="true" 并 disabled,防止重复提交。
    • 使用 aria-live="polite" 的隐藏文本提示用户当前状态。
    • Spinner 使用 aria-hidden="true",避免重复朗读。
  • 性能优化:
    • 使用 transform 进行按下微交互,减少布局重排。
    • 过渡动画时间短(≤120ms),在 reduced-motion 下禁用。
    • 通过 CSS 变量统一主题与品牌色,减少多产品线维护成本。
  • 多平台适配:
    • 悬停状态通过 @media (hover: hover) 条件启用;移动端主要依赖 pressed 与 focus-visible。
    • 高度 L(48px)用于移动触控确保≥44px最小目标;桌面密度可用 M/S。

扩展与可复用建议:

  • 将颜色与尺寸定义为设计令牌(JSON/CSS variables),在不同产品线通过主题层覆盖。
  • 提供“带图标”“全宽”“危险主按钮(红)”“次要按钮”变体规范,保持结构一致、仅替换令牌与交互细节。
  • 在组件库中封装通用属性:size、loading、disabled、icon、ariaLabel,并统一事件节流与加载状态管理。

1. 组件类型和设计优先级说明

  • 组件类型:文本输入框(含标签、占位、长度限制、校验、错误提示)
  • 使用场景:表单、搜索、账号信息输入、配置项编辑等
  • 设计优先级:
    • 可用性:清晰标签与提示、即时且不打扰的校验反馈、明确的错误信息、可见的字符计数
    • 可访问性:语义化标签关联、焦点可见、对比度达标、屏幕阅读器可读、键盘与触控友好
    • 响应式设计:宽度随容器自适应,尺寸规格(S/M/L)适配移动与桌面
    • 视觉一致性:统一色板、字号、间距和状态样式;跨产品线可用的设计令牌(Design Tokens)

2. 视觉属性(颜色、字体、间距、尺寸等)

建议通过设计令牌提供主题化支持(浅色/深色/高对比模式),以下为默认值与命名示例。

  • 结构层级

    • 标签(Label):置于输入框上方,左对齐
    • 输入区(Input):支持前缀/后缀、占位符、清除按钮、显示/隐藏密码按钮(可选)
    • 辅助文案(Helper Text):位于输入框下方,提供说明或格式提示
    • 错误提示(Error Text):位于辅助文案下方,出现时优先显示
    • 字符计数(Character Counter):位于右下角,与错误提示同一行右侧对齐
  • 颜色(Design Tokens)

    • --color-bg-surface: #FFFFFF(浅色)/ #1C1C1C(深色)
    • --color-bg-input: #FFFFFF(浅色)/ #121212(深色)
    • --color-border: #C9CED6
    • --color-border-hover: #AEB6C2
    • --color-border-focus: #2563EB(可用主题主色,需对比度达标)
    • --color-text: #0F172A
    • --color-placeholder: #6B7280(对比度≥4.5:1 推荐)
    • --color-label: #334155
    • --color-helper: #64748B
    • --color-error: #DC2626
    • --color-success: #16A34A
    • --focus-ring: rgba(37, 99, 235, 0.35)
  • 字体与字号

    • 标签:Medium,12–14px(推荐 12px/0.75rem),行高 18–20px
    • 输入文字:Regular,14–16px(推荐 14px/0.875rem),行高 20–22px
    • 占位符:Regular,14px,颜色使用 --color-placeholder
    • 辅助/错误文案:Regular,12–13px(推荐 12px),行高 16–18px
    • 计数器:Regular,12px
  • 尺寸规格(高度,含内边距,不含外边距)

    • S:高度 36px,左右内边距 12px
    • M(默认):高度 40px,左右内边距 12–16px
    • L:高度 48px,左右内边距 16–20px
    • 圆角:6px(与系统/品牌风格一致)
    • 边框:1px,错误/焦点状态颜色变化
    • 图标区宽度:16–20px,左右预留 8px 间距
  • 间距与布局

    • 标签与输入框间距:6–8px
    • 输入框与辅助文案间距:6px
    • 辅助文案与错误提示间距:4px(错误出现时覆盖辅助文案信息)
    • 字符计数与错误提示同行:左右分布,两端对齐
    • 容器宽度:默认 100%,最大宽度由表单布局控制(如 320–640px)
  • 视觉变体(可选)

    • 轮廓(Outlined):推荐默认,边框清晰、对比度好
    • 填充(Filled):背景轻微填充(如 #F8FAFC),边框弱化,聚焦时轮廓强调

3. 交互状态(默认、悬停、点击、禁用等)

  • 默认(Default)

    • 边框:--color-border
    • 背景:--color-bg-input
    • 占位符显示,标签在上方;若为必填显示星号(*)且与标签同色或强调色
  • 悬停(Hover)

    • 边框:--color-border-hover
    • 光标:文本输入型(I-beam)
  • 聚焦(Focus)

    • 边框:--color-border-focus
    • 焦点可视化:外发光或内阴影,使用 --focus-ring(2–3px)
    • 键盘导航:Tab 进入,Shift+Tab 退出;Esc 关闭展开的辅助功能(如密码可见菜单)
  • 激活/输入中(Active/Typing)

    • 文本光标可见;占位符在有值后消失;字符计数同步更新
  • 已填(Filled)

    • 占位符隐藏;标签保持位置(不使用浮动标签以避免可访问性问题)
  • 错误(Invalid)

    • 边框:--color-error
    • 错误文案显示:优先级高于辅助文案;aria-live 更新
    • 字段状态:aria-invalid="true"
  • 警示(Warning,可选)

    • 边框:#F59E0B;提供格式提醒但不阻止提交
  • 成功(Valid,可选)

    • 边框:--color-success;成功图标可选,但避免视觉干扰
  • 禁用(Disabled)

    • 背景:#F1F5F9(浅色)/ #2A2A2A(深色)
    • 文本:#94A3B8;占位符同色系更浅
    • 交互:不可编辑、不可聚焦;光标默认
  • 只读(Read-only)

    • 背景:与默认一致或轻微填充
    • 交互:可聚焦以便复制,但不可编辑
  • 附加行为

    • 清除按钮:当有内容时显示(X),点击清空并保持焦点;键盘支持(Space/Enter)
    • 密码显隐:切换 type=password/text;按钮需有可读文本与状态说明
    • 前缀/后缀:如货币符号、单位;不影响可编辑区域宽度与光标位置

4. 可访问性考虑(辅助功能、可读性、对比度等)

  • 语义与关联

    • 使用 label 元素并通过 for/id 与 input 关联
    • 通过 aria-describedby 关联辅助文案、错误提示、计数器的 id
    • 必填:在视觉上用星号或“必填”提示,并使用 aria-required="true"
    • 错误状态:设置 aria-invalid="true",错误容器 aria-live="polite"(或 role="alert" 提交后)
  • 对比度

    • 文本与背景对比度 ≥ 4.5:1(小文本)
    • 边框与背景对比度 ≥ 3:1(确保可见)
    • 占位符推荐 ≥ 4.5:1 或至少 3:1 并提供标签以避免识别困难
  • 键盘与屏幕阅读器

    • Tab 聚焦输入框;Enter 触发表单提交(如在表单内);Esc 清除弹出式辅助
    • 清除/密码显隐按钮可聚焦且有可读 label(aria-label)
    • 计数器应以剩余字符形式可读(如“还可输入 12 个字符”)
  • 输入类型与辅助属性

    • 使用恰当的 type:text、email、url、tel、password、number 等
    • inputmode 与 autocomplete:提升移动端可用性(如 email、tel、one-time-code)
    • spellcheck:根据场景开启/关闭;数字类关闭,自由文本可开启
    • RTL 支持:前缀/后缀与文字方向自适应;光标与选择行为符合语言方向
  • 错误信息文案规范

    • 清晰具体、可行动:“请输入有效的邮箱地址(例如 name@example.com)”
    • 一次只显示一条最相关错误;避免技术性术语(如“正则不匹配”)

5. 代码实现说明(HTML/CSS/JS 示例或组件代码规范)

示例一:基础输入(带标签、占位、计数、校验、错误提示)

<div class="field" data-size="m">
  <label for="email" class="field__label">
    邮箱地址 <span class="field__required" aria-hidden="true">*</span>
  </label>

  <div class="field__control">
    <!-- 可选前缀 -->
    <!-- <span class="field__prefix" aria-hidden="true">@</span> -->

    <input
      id="email"
      name="email"
      type="email"
      class="field__input"
      placeholder="name@example.com"
      required
      aria-required="true"
      maxlength="100"
      autocomplete="email"
      inputmode="email"
      aria-invalid="false"
      aria-describedby="email_helper email_counter email_error"
    />

    <!-- 可选后缀或清除按钮/密码显隐按钮 -->
    <!-- <button type="button" class="field__clear" aria-label="清除输入" hidden>×</button> -->
  </div>

  <div id="email_helper" class="field__helper">
    我们仅用于账户通知,不会发送垃圾邮件。
  </div>

  <div class="field__meta">
    <div id="email_error" class="field__error" aria-live="polite"></div>
    <div id="email_counter" class="field__counter" aria-live="polite" aria-atomic="true">0/100</div>
  </div>
</div>

CSS(使用设计令牌,支持浅/深主题)

:root {
  --color-bg-surface: #FFFFFF;
  --color-bg-input: #FFFFFF;
  --color-border: #C9CED6;
  --color-border-hover: #AEB6C2;
  --color-border-focus: #2563EB;
  --color-text: #0F172A;
  --color-placeholder: #6B7280;
  --color-label: #334155;
  --color-helper: #64748B;
  --color-error: #DC2626;
  --color-success: #16A34A;
  --focus-ring: rgba(37, 99, 235, 0.35);

  --radius: 6px;
  --space-2: 8px;
  --space-1: 4px;

  --height-s: 36px;
  --height-m: 40px;
  --height-l: 48px;
}

.field { width: 100%; }
.field__label {
  display: block;
  margin-bottom: 6px;
  color: var(--color-label);
  font-size: 12px;
  line-height: 18px;
}
.field__required { color: var(--color-error); margin-left: 4px; }

.field__control {
  position: relative;
  display: flex;
  align-items: center;
  background: var(--color-bg-input);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.field[data-size="s"] .field__control { height: var(--height-s); }
.field[data-size="m"] .field__control { height: var(--height-m); }
.field[data-size="l"] .field__control { height: var(--height-l); }

.field__input {
  flex: 1;
  width: 100%;
  border: 0;
  outline: none;
  background: transparent;
  color: var(--color-text);
  font-size: 14px;
  line-height: 20px;
  padding: 0 12px;
}
.field__input::placeholder { color: var(--color-placeholder); }

.field__control:hover { border-color: var(--color-border-hover); }
.field__input:focus-visible + .field__suffix,
.field__input:focus-visible {
  /* focus ring on container for robust visuals */
}
.field__control:has(.field__input:focus-visible) {
  border-color: var(--color-border-focus);
  box-shadow: 0 0 0 3px var(--focus-ring);
}

.field__helper {
  margin-top: 6px;
  color: var(--color-helper);
  font-size: 12px;
  line-height: 16px;
}

.field__meta {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-top: 4px;
}
.field__error {
  min-height: 16px;
  color: var(--color-error);
  font-size: 12px;
  line-height: 16px;
}
.field__counter {
  color: var(--color-helper);
  font-size: 12px;
  line-height: 16px;
}

/* 错误状态(容器类控制) */
.field.is-invalid .field__control {
  border-color: var(--color-error);
  box-shadow: none;
}
.field.is-disabled .field__control {
  background: #F1F5F9;
  border-color: #E2E8F0;
}
.field.is-disabled .field__input {
  color: #94A3B8;
  cursor: not-allowed;
}

JS(校验、计数、a11y 更新)

function initTextField(root) {
  const input = root.querySelector('.field__input');
  const error = root.querySelector('.field__error');
  const counter = root.querySelector('.field__counter');

  const max = input.maxLength > 0 ? input.maxLength : null;

  function updateCounter() {
    if (!counter || !max) return;
    const len = input.value.length;
    counter.textContent = `${len}/${max}`;
  }

  function validate() {
    let msg = '';
    // HTML 原生校验
    if (input.validity.valueMissing) {
      msg = '此字段为必填项。';
    } else if (input.type === 'email' && input.value && input.validity.typeMismatch) {
      msg = '请输入有效的邮箱地址(例如 name@example.com)。';
    } else if (input.value && input.minLength && input.value.length < input.minLength) {
      msg = `至少输入 ${input.minLength} 个字符。`;
    } else if (max && input.value.length > max) {
      msg = `最多输入 ${max} 个字符。`;
    }

    const invalid = !!msg;
    root.classList.toggle('is-invalid', invalid);
    input.setAttribute('aria-invalid', invalid ? 'true' : 'false');
    if (error) error.textContent = msg;
  }

  input.addEventListener('input', () => {
    updateCounter();
    // 实时但不打扰:仅在已输入内容后做轻量校验
    validate();
  });
  input.addEventListener('blur', validate);

  // 初始化
  updateCounter();
  validate();
}

document.querySelectorAll('.field').forEach(initTextField);

实现要点与规范

  • 标签与输入关联:label[for] 与 input[id] 必须一一对应
  • 错误提示与辅助文案:分别赋予唯一 id,并通过 aria-describedby 绑定
  • 计数器:使用 maxlength 属性与动态文本同步,屏幕阅读器通过 aria-live 获取剩余信息
  • 表单提交策略:提交时统一校验,错误容器可设置 role="alert" 以便即刻播报
  • 响应式:容器宽度 100%,在表单栅格中控制最大宽度;在移动端使用合适的 inputmode(如 email、tel)
  • 交互按钮(清除、密码显隐)
    • 清除按钮在有内容时显示,使用 aria-label,按下后保持焦点在输入框
    • 密码显隐按钮需同步更新 aria-pressed 与 aria-label(如“显示密码/隐藏密码”)
  • 多语言与 RTL:错误文案可本地化;通过 dir="rtl" 支持从右到左布局;前缀/后缀在 flex 方向下自动适配
  • 高对比模式:边框与焦点环不依赖仅颜色变化;提供清晰的轮廓与阴影层级

扩展变体(可选)

  • 带前缀/后缀:用于单位、货币、协议(https://);确保不影响输入可选中区域
  • 只读/禁用:分别用于不可编辑但可复制、完全不可交互的场景
  • 校验类型:email、url、tel、number、regex(pattern),统一错误文案风格与位置

此规范保证跨产品线的复用与一致性,通过令牌化的视觉属性、标准化的状态与可访问性处理,以及清晰的开发示例,支持快速实现与主题扩展。

1. 组件类型和设计优先级说明

组件类型:标签页导航(Tabs,水平为主,支持宽度自适应、溢出处理、键盘导航与焦点管理)

设计优先级(从高到低):

  1. 可用性:清晰的选中态与层级关系、易识别标签、最小点击/触控尺寸、溢出时的可达与可见
  2. 响应式设计:在不同屏幕与容器宽度下自适应布局,平滑处理溢出和断点
  3. 可访问性:符合 WAI-ARIA Tabs 模式,完善的键盘交互、焦点可见性、对比度与语义
  4. 视觉一致性:统一的样式令牌(Design Tokens),一致的状态与动效
  5. 性能优化:纯 CSS 优先、最小重绘与重排、轻量事件与观察者、渐进增强

适用范围:多产品线的顶部标签导航;支持内容面板切换、可滚动标签列、移动端触控与桌面端键盘/鼠标。


2. 视觉属性(颜色、字体、间距、尺寸等)

采用设计令牌(CSS变量)以保证跨产品一致性与可复用。

  • 容器与方向

    • 默认方向:水平(aria-orientation="horizontal")
    • 支持 RTL 文本方向;所有箭头与溢出按钮镜像处理
  • 尺寸与间距

    • 高度:--tab.height = 40–48px(桌面),--tab.height.touch = 44–52px(移动)
    • 内边距:--tab.padding.inline = 12–16px;--tab.padding.block = 8–10px
    • 最小宽度:--tab.minWidth = 72px(保证短标签点击区域)
    • 间距(标签间):--tab.gap = 8px
    • 选中指示条(Active Indicator):
      • 高度:--tab.indicator.height = 2px
      • 位置:底部(水平),左侧(垂直)
  • 字体

    • 字号:--tab.font.size = 14–16px
    • 行高:--tab.font.lineHeight = 1.4–1.6
    • 字重:--tab.font.weight = 500(默认),600(选中)
  • 颜色(浅色主题为例,深色主题需对应反转)

    • 文本:--color.tab.text = #1F2937;选中:--color.tab.text.active = #111827;禁用:--color.tab.text.disabled = #9CA3AF
    • 背景:--color.tab.bg = transparent;悬停:--color.tab.bg.hover = #F3F4F6;选中:--color.tab.bg.active = transparent
    • 指示条:--color.tab.indicator = #2563EB
    • 分隔线(tabbar底部):--color.tabbar.border = #E5E7EB
    • 溢出渐隐遮罩(可选):线性渐变至透明,--color.fade = #FFFFFF(浅色),深色主题改为背景色
    • 溢出按钮(左右箭头):
      • 文本/图标:--color.overflow.icon = #6B7280(默认),悬停:#111827
      • 背景:--color.overflow.bg.hover = #F3F4F6;禁用文本:#D1D5DB
  • 边框与圆角

    • 标签圆角:--tab.radius = 6px(非必要,示例风格)
    • 指示条无圆角或半径 1px
    • 标签无边框,仅在 hover 显示轻微背景
  • 截断与徽标

    • 文本截断:单行,text-overflow: ellipsis;最大宽度:--tab.maxWidth = 200–240px
    • 徽标/计数(可选):右侧对齐,字号 12–13px,背景对比度不影响整体对比(示例:灰底白字或蓝底白字)
  • 动效

    • 指示条移动:transform + transition 150–200ms(ease-out);尊重 prefers-reduced-motion
    • 溢出按钮与渐隐遮罩淡入/淡出:opacity 120ms

3. 交互状态(默认、悬停、点击、禁用等)

  • 默认(Rest)

    • 文本使用 --color.tab.text
    • 背景透明
  • 悬停(Hover)

    • 背景:--color.tab.bg.hover
    • 文本稍微加深或不变(保持对比度)
  • 聚焦(Focus-visible)

    • 可见焦点环:2px 外轮廓,颜色 --focus.ring = #2563EB,outline-offset = 2px
    • 指示条不变
  • 选中(Active / Selected)

    • 文本:--color.tab.text.active;字重 600
    • 指示条:显示,颜色 --color.tab.indicator
  • 按下(Pressed)

    • 背景轻微压暗或缩放 0.98(仅视觉,避免文本位移)
  • 禁用(Disabled)

    • 文本:--color.tab.text.disabled
    • 背景:透明;不可点、不可聚焦(tabindex 与 aria-disabled 管理)
  • 溢出按钮(左右导航)

    • 默认:icon 使用 --color.overflow.icon
    • 悬停:背景 --color.overflow.bg.hover,icon 加深
    • 禁用:icon 变淡,cursor: not-allowed
  • 触控反馈(移动端)

    • 使用 :active 显示轻微背景;确保触控目标≥44px
  • 溢出处理交互策略(两种可选,可并存)

    1. 可滚动标签列:在容器两侧显示渐隐遮罩与左右按钮,点击/键盘触发 scroll;活动标签始终自动滚入可视区
    2. “更多”菜单:超出容器的标签收纳至 More 菜单(下拉),激活后将其移至主列或保持当前选中可见(根据产品策略)

4. 可访问性考虑(辅助功能、可读性、对比度等)

  • 语义与角色
    • 容器:role="tablist",aria-orientation="horizontal"(或 "vertical")
    • 标签:role="tab",aria-selected="true|false",aria-controls="[tabpanel-id]"
    • 面板:role="tabpanel",aria-labelledby="[tab-id]",非活动面板隐藏(hidden 或 aria-hidden),并移出读屏焦点流(tabindex="-1")
  • 键盘导航(遵循 WAI-ARIA Authoring Practices)
    • 左/右箭头:在同一组内移动焦点(roving tabindex)
    • Home/End:跳到第一/最后一个标签
    • Enter/Space:激活当前聚焦标签并显示对应面板
    • Ctrl+PageUp/PageDown(桌面可选增强):在相邻标签间循环切换激活
    • 溢出按钮可聚焦,左右箭头键与 Enter/Space 可触发滚动
  • 焦点管理
    • 仅一个标签为 tabindex="0",其他为 "-1"(roving)
    • 激活后将焦点保留在标签上;若业务需要将焦点移至面板,确保面板可聚焦并将其滚入视区
  • 对比度
    • 文本与背景对比度≥4.5:1;指示条与背景≥3:1
    • 高对比模式支持(使用 outline 而非仅颜色变化)
  • 读屏与公告
    • 切换面板时,不使用 assertive live;保持自然阅读顺序
    • 若标签文本被截断,提供完整文本:title 属性或 aria-label
  • 触控与命中区域
    • 最小触控目标 44×44px
    • 横向滚动支持触控滑动;禁用过度动画,确保可预期
  • 语言与方向
    • RTL 支持:箭头与滚动方向镜像;使用 logical properties(inline-start/inline-end)
  • 降级与无动画
    • prefers-reduced-motion: reduce 时禁用位移动画与滚动平滑
  • 无障碍测试清单
    • Tab 键顺序正确、焦点环可见
    • 箭头/Home/End/Enter/Space/(可选 Ctrl+PgUp/PgDn)行为正确
    • 屏幕阅读器朗读标签与面板标题正确,aria-selected 切换及时
    • 溢出时,所有标签仍可通过键盘或 More 菜单访问

5. 代码实现说明(HTML/CSS/JS示例或组件代码规范)

示例:滚动型 Tabs(带溢出左右按钮),语义与键盘完整,使用设计令牌

HTML

<div class="tabs" dir="ltr">
  <div class="tabs-bar" role="tablist" aria-orientation="horizontal">
    <button class="overflow-btn prev" aria-label="Scroll left" disabled></button>
    <div class="tabs-scroll" data-scroll>
      <!-- 每个 tab 使用 button + role="tab"(可获得可点击与可聚焦语义) -->
      <button id="tab-1" role="tab" class="tab is-selected" aria-selected="true"
              aria-controls="panel-1" tabindex="0" title="概览">概览</button>
      <button id="tab-2" role="tab" class="tab" aria-selected="false"
              aria-controls="panel-2" tabindex="-1" title="报表与分析">报表与分析</button>
      <button id="tab-3" role="tab" class="tab" aria-selected="false"
              aria-controls="panel-3" tabindex="-1" title="设置">设置</button>
      <!-- ... 更多标签 -->
    </div>
    <button class="overflow-btn next" aria-label="Scroll right"></button>
  </div>

  <section id="panel-1" role="tabpanel" aria-labelledby="tab-1">面板内容1</section>
  <section id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>面板内容2</section>
  <section id="panel-3" role="tabpanel" aria-labelledby="tab-3" hidden>面板内容3</section>
</div>

CSS(设计令牌与样式)

:root {
  --tab.height: 44px;
  --tab.padding.inline: 16px;
  --tab.padding.block: 10px;
  --tab.minWidth: 72px;
  --tab.maxWidth: 240px;
  --tab.gap: 8px;
  --tab.radius: 6px;
  --tab.indicator.height: 2px;

  --tab.font.size: 14px;
  --tab.font.lineHeight: 1.5;
  --tab.font.weight: 500;
  --tab.font.weight.active: 600;

  --color.tab.text: #1F2937;
  --color.tab.text.active: #111827;
  --color.tab.text.disabled: #9CA3AF;
  --color.tab.bg.hover: #F3F4F6;
  --color.tabbar.border: #E5E7EB;
  --color.tab.indicator: #2563EB;

  --color.overflow.icon: #6B7280;
  --color.overflow.icon.hover: #111827;
  --color.overflow.bg.hover: #F3F4F6;

  --focus.ring: #2563EB;
}

.tabs { display: flex; flex-direction: column; gap: 12px; }
.tabs-bar {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  border-bottom: 1px solid var(--color.tabbar.border);
  contain: content; /* 性能优化:限制布局影响范围 */
}

.tabs-scroll {
  display: flex;
  gap: var(--tab.gap);
  overflow-x: auto;
  scrollbar-width: none; /* Firefox 隐藏滚动条 */
}
.tabs-scroll::-webkit-scrollbar { display: none; } /* WebKit 隐藏滚动条 */

/* 渐隐遮罩:可选(结合 mask 或伪元素) */
.tabs-scroll {
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);
}

.tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--tab.height);
  padding: var(--tab.padding.block) var(--tab.padding.inline);
  min-width: var(--tab.minWidth);
  max-width: var(--tab.maxWidth);
  border: 0;
  border-radius: var(--tab.radius);
  background: transparent;
  color: var(--color.tab.text);
  font-size: var(--tab.font.size);
  line-height: var(--tab.font.lineHeight);
  font-weight: var(--tab.font.weight);
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  cursor: pointer;
}

.tab:hover { background: var(--color.tab.bg.hover); }
.tab:focus-visible {
  outline: 2px solid var(--focus.ring);
  outline-offset: 2px;
}

/* 选中态:底部指示条 */
.tab.is-selected {
  font-weight: var(--tab.font.weight.active);
  color: var(--color.tab.text.active);
}
.tab.is-selected::after {
  content: "";
  position: absolute;
  left: 8px; right: 8px; bottom: -1px;
  height: var(--tab.indicator.height);
  background: var(--color.tab.indicator);
  border-radius: 1px;
}

.tab[aria-disabled="true"] {
  color: var(--color.tab.text.disabled);
  cursor: not-allowed;
}

.overflow-btn {
  inline-size: 32px; block-size: 32px; /* 触控可改 40–44 */
  border: 0; border-radius: 6px;
  background: transparent;
  cursor: pointer;
  position: relative;
}
.overflow-btn:focus-visible { outline: 2px solid var(--focus.ring); outline-offset: 2px; }
.overflow-btn:hover { background: var(--color.overflow.bg.hover); }
.overflow-btn[disabled] { opacity: 0.5; cursor: not-allowed; }

/* 使用伪元素绘制箭头,或替换为内嵌SVG */
.overflow-btn.prev::before, .overflow-btn.next::before {
  content: "";
  display: inline-block;
  width: 0; height: 0;
  border-top: 6px solid transparent; border-bottom: 6px solid transparent;
  border-inline-start: 6px solid var(--color.overflow.icon); /* 三角形箭头 */
}
.overflow-btn.next::before {
  transform: scaleX(-1); /* 镜像为右箭头 */
}

/* 面板样式示例 */
[role="tabpanel"] { padding: 12px 0; }
[role="tabpanel"][hidden] { display: none; }

/* 优化:减少动画 */
@media (prefers-reduced-motion: reduce) {
  .tab.is-selected::after { transition: none; }
}

JavaScript(键盘与溢出滚动、roving tabindex、激活与面板切换)

class Tabs {
  constructor(root) {
    this.root = root;
    this.tablist = root.querySelector('[role="tablist"]');
    this.scroll = root.querySelector('.tabs-scroll');
    this.prevBtn = root.querySelector('.overflow-btn.prev');
    this.nextBtn = root.querySelector('.overflow-btn.next');
    this.tabs = Array.from(root.querySelectorAll('[role="tab"]'));
    this.panels = Array.from(root.querySelectorAll('[role="tabpanel"]'));

    this._bind();
    this._updateOverflow();
    this._ensureSelectedVisible();
    this._observeResize();
  }

  _bind() {
    this.tablist.addEventListener('keydown', (e) => this._onKeydown(e), { passive: true });
    this.tabs.forEach(tab => {
      tab.addEventListener('click', () => this.selectTab(tab));
    });

    // 溢出滚动
    this.prevBtn?.addEventListener('click', () => this._scrollBy(-1));
    this.nextBtn?.addEventListener('click', () => this._scrollBy(1));
    this.scroll?.addEventListener('scroll', () => this._updateOverflow(), { passive: true });
  }

  _onKeydown(e) {
    const rtl = getComputedStyle(this.tablist).direction === 'rtl';
    const currentIndex = this.tabs.indexOf(document.activeElement);

    // 仅在 tab 获得焦点时处理
    if (currentIndex === -1) return;

    const key = e.key;
    let nextIndex = null;

    if (key === 'ArrowRight') {
      nextIndex = rtl ? currentIndex - 1 : currentIndex + 1;
    } else if (key === 'ArrowLeft') {
      nextIndex = rtl ? currentIndex + 1 : currentIndex - 1;
    } else if (key === 'Home') {
      nextIndex = 0;
    } else if (key === 'End') {
      nextIndex = this.tabs.length - 1;
    } else if (key === 'Enter' || key === ' ') {
      e.preventDefault();
      this.selectTab(this.tabs[currentIndex]);
      return;
    } else if (key === 'PageUp' && e.ctrlKey) {
      nextIndex = (currentIndex - 1 + this.tabs.length) % this.tabs.length;
    } else if (key === 'PageDown' && e.ctrlKey) {
      nextIndex = (currentIndex + 1) % this.tabs.length;
    }

    if (nextIndex != null) {
      e.preventDefault();
      nextIndex = Math.max(0, Math.min(this.tabs.length - 1, nextIndex));
      const nextTab = this.tabs[nextIndex];
      this._moveFocus(nextTab);
      this._scrollIntoView(nextTab);
    }
  }

  selectTab(tab) {
    if (tab.getAttribute('aria-disabled') === 'true') return;

    // 更新选中状态与 roving tabindex
    this.tabs.forEach(t => {
      const selected = t === tab;
      t.classList.toggle('is-selected', selected);
      t.setAttribute('aria-selected', String(selected));
      t.tabIndex = selected ? 0 : -1;
    });

    // 面板切换
    const panelId = tab.getAttribute('aria-controls');
    this.panels.forEach(panel => {
      const active = panel.id === panelId;
      panel.toggleAttribute('hidden', !active);
      if (!active) panel.tabIndex = -1;
      else panel.tabIndex = 0; // 若需将焦点移入面板,可在此 focus()
    });

    this._scrollIntoView(tab);
    this._updateOverflow();
  }

  _moveFocus(tab) { tab.focus({ preventScroll: true }); }

  _scrollBy(direction) {
    const step = this.scroll.clientWidth * 0.8; // 按容器宽度滚动
    const delta = direction * step;
    this.scroll.scrollBy({ left: delta, behavior: this._reducedMotion() ? 'auto' : 'smooth' });
  }

  _scrollIntoView(tab) {
    const rect = tab.getBoundingClientRect();
    const sRect = this.scroll.getBoundingClientRect();
    const isLeftOverflow = rect.left < sRect.left + 8;
    const isRightOverflow = rect.right > sRect.right - 8;

    if (isLeftOverflow) {
      const offset = rect.left - sRect.left - 8;
      this.scroll.scrollBy({ left: offset, behavior: this._reducedMotion() ? 'auto' : 'smooth' });
    } else if (isRightOverflow) {
      const offset = rect.right - sRect.right + 8;
      this.scroll.scrollBy({ left: offset, behavior: this._reducedMotion() ? 'auto' : 'smooth' });
    }
  }

  _updateOverflow() {
    if (!this.scroll || !this.prevBtn || !this.nextBtn) return;
    const maxScroll = this.scroll.scrollWidth - this.scroll.clientWidth;
    const x = Math.round(this.scroll.scrollLeft);

    this.prevBtn.disabled = x <= 0;
    this.nextBtn.disabled = x >= maxScroll - 1;
  }

  _observeResize() {
    // ResizeObserver:尺寸变化时更新溢出状态
    const ro = new ResizeObserver(() => {
      this._updateOverflow();
      this._ensureSelectedVisible();
    });
    ro.observe(this.scroll);
    ro.observe(this.tablist);
    this._ro = ro;
  }

  _ensureSelectedVisible() {
    const selected = this.tabs.find(t => t.classList.contains('is-selected'));
    if (selected) this._scrollIntoView(selected);
  }

  _reducedMotion() {
    return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  }
}

// 初始化(支持多实例)
document.querySelectorAll('.tabs').forEach(el => new Tabs(el));

组件代码规范与扩展建议

  • 结构约定
    • 使用
    • 面板与标签通过 aria-controls/aria-labelledby 成对关联;非活动面板 hidden
  • 响应式与溢出策略
    • 小屏首选“可滚动标签列”,确保触控与键盘的可达性;可选在更小断点将多余标签折叠为 More 菜单
    • 使用 ResizeObserver 与 CSS logical properties 以适配容器变化与 RTL
  • 性能优化
    • 事件监听使用 passive;滚动与键盘处理避免强制同步布局
    • 使用 CSS contain 限制重排范围;避免频繁测量所有标签,仅在需要时 getBoundingClientRect
    • 指示条位移使用 transform;尊重 prefers-reduced-motion
  • 可测试性
    • 提供数据属性(data-tab-id)以便端到端测试定位
    • 对外暴露激活回调(onSelect)与选择方法(selectTabById),便于集成状态管理
  • 无障碍一致性
    • 保持 roving tabindex;焦点环始终可见;截断文本通过 title 或 aria-label 提供完整读法

此规范确保标签页导航在多设备与多产品线下具备一致的视觉与交互体验,满足可用性、响应式、可访问性与性能优化要求,并提供直接可用的实现参考。

示例详情

解决的问题

帮助用户高效制定UI组件设计规范,确保设计的一致性和高质量交付,同时满足视觉美观、交互体验和代码实现等多维需求。

适用用户

UI设计师

快速生成完整的设计规范文档,减少重复性工作,专注于设计创新。

产品经理

为产品输出清晰可执行的UI组件规则,确保团队一致性协作。

前端开发工程师

借助自动生成的代码说明,提高开发效率,减少对设计师反复沟通的时间。

特征总结

轻松生成高质量的UI组件设计规范,快速覆盖视觉、交互及代码实现全维度。
自动整合交互状态,确保设计方案对不同功能和场景表现一致性强。
一键输出可访问性考虑,为产品开发提供无障碍设计基础支持。
针对不同UX优先级自动优化设计规范,满足体验目标的精准需求。
支持灵活定制组件类型及优先级,迅速适配各类产品设计需求场景。
生成的设计规范包含代码实现方向,为设计到开发全流程协作提供实际指导。
优化设计文档的标准化与规范度,帮助团队提升设计与交付效率。

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥20.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 590 tokens
- 2 个可调节参数
{ 组件类型 } { 优先考虑 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
限时免费

不要错过!

免费获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59