Figma 数字输入框与数值步进器系统(InputNumber & Stepper Architecture)组件架构实战:从上下微调按钮对齐、前后置单位加成、极值边界(Min/Max/Step)防溢出到 Variables 实时数学计算与 Dev Mode 前端代码交付
一、引言与架构全景:为什么数字输入框是量化业务系统的精确交互中枢? #
在金融资产交易、电商购物车与库存调度、云服务资源规格配额(如计算核数、内存容量配置),以及专业生产力软件(包括 Figma 自身的属性检查面板、3D 建模工具等)中,数字输入框与数值步进器(InputNumber & Stepper) 是承担离散量化数据精准输入的核心组件。
与通用的纯文本输入框(Text Input)允许自由输入字符串不同,数字输入框面临着更严苛的数据数学确定性、精度控制与空间对齐规范:
- 数值增减时界面抖动与晃动(Layout Jittering):在默认的可变宽排印字体(Proportional Figures)下,数字
1的物理字符宽度明显窄于数字8或0。当用户点击微调按钮使数值在19与20之间连续切换时,光标、后置单位或右侧按钮会发生剧烈的左右水平抖动,严重伤害用户体验; - 微调手柄(Stepper Handler)在 Auto Layout 下像素级对齐错位:上下微调箭头按钮高度通常只有
12px ~ 16px,设计师在未使用规范 Auto Layout 比例锁定时,一旦输入框高度从32px切换为40px,中间分割线即刻偏位,箭头图标发生光学失真; - 极值边界(Min / Max / Step)缺乏联动防御:设计稿往往只画出了一个合法的数值居中状态,忽略了“数值达到下限 0 时减号禁用”、“达到库存上限时加号置灰拦截”以及“支持任意浮点步长(如 0.01 / 0.5)”的正交边界;
- 原型脱离真实计算,无法进行联动演示:高保真原型评审时,加减按钮点击无响应,无法驱动同页面的“购物车总价”或“资产配额图表”实时刷新;
- Dev Mode 前端属性丢失:前端工程师在检查标注时,只看到普通的静态矩形,缺少原生 HTML5
<input type="number">的min、max、step、aria-valuenow等无障碍可访问性属性的精准标注。
为了从根本上消除设计与工程落地之间的鸿沟,我们将企业级数字输入框与步进器系统解构为四层拓扑架构:
+------------------------------------------------------------------------+
| Figma 数字输入框与数值步进器系统(InputNumber System)四层拓扑结构 |
+------------------------------------------------------------------------+
| 1. 双拓扑 Auto Layout 几何架构层 (Dual-Topology Geometry Layer) |
| - 拓扑 A:内嵌紧凑微调型 (Inline Stacked Controls, 适合桌面端控制台) |
| - 拓扑 B:两端分离加减型 (Split Stepper Controls, 适合触控屏与购物车)|
| - 响应式等分约束与垂直居中锁定 (Vertical / Horizontal Auto Layout) |
+------------------------------------------------------------------------+
| 2. 等宽排印与前后缀单位层 (Tabular Figures & Prefix/Suffix Tokens) |
| - OpenType 等宽数字特性 (Number: Tabular Figures 抗抖动) |
| - 前置货币/符号插槽 (Prefix Slot: ¥ / $ / ±) |
| - 后置度量单位插槽 (Suffix Slot: % / px / kg / 台) |
+------------------------------------------------------------------------+
| 3. 极值状态机与步长控制器 (Min/Max/Step & Guard State Layer) |
| - 步长递增/递减算法 (Step Interval: 1, 0.1, 10) |
| - 极值越界防护 (Lower Bound: min / Upper Bound: max) |
| - 智能禁用矩阵 (At Min -> Decrement Disabled, At Max -> Increment Dis)|
+------------------------------------------------------------------------+
| 4. Variables 驱动与 Dev Mode 交付层 (Variables & Engineering Layer) |
| - 数值变量表达式驱动 (currentValue, step, min, max, isAtBoundary) |
| - 交互式连续加减与跨组件数值联动原型闭环 |
| - React + TypeScript + WAI-ARIA spinbutton 生产级代码无损映射 |
+------------------------------------------------------------------------+
二、双拓扑 Auto Layout 几何架构:内嵌微调按钮 vs 两端分离加减按钮 #
根据终端物理形态与输入密度的差异,数字输入框主要分化为两大经典拓扑形式:
1. 拓扑 A:内嵌紧凑微调型(Inline Stacked Controls) #
常见于 SaaS 控制台、参数调优面板(如设计软件的宽高调整、财务汇率微调)。其核心特征是整体占用面积小,右侧垂直并列两个高密度微调箭头(Up/Down)。
+--------------------------------------------------+
| 前缀 [¥] 数值文本 [ 1,280.00 ] 后缀 [RMB] | [▲] |
| | [▼] |
+--------------------------------------------------+
Auto Layout 搭建步骤: #
- 构建右侧微调手柄容器:
- 创建一个名为
Stepper/Controls的 Frame,按下Shift + A设为 Vertical Auto Layout; - 宽度固定设置为
20px或24px,高度设置为 Fill container; - 内部包含两个等分按钮(
Btn/Up与Btn/Down),高度均设为 Fill container,确保上下高度各占 50%; - 中间通过 1px 的内嵌描边(Border Bottom / Center Divider)进行物理分割,内嵌
8px向上/向下微型三角箭头,图标开启居中约束;
- 创建一个名为
- 组合主体输入框:
- 包含前缀插槽(Prefix)、数值显示文本(Value Text,设置为 Fill container 且垂直居中)、后缀插槽(Suffix);
- 水平外层容器设为 Horizontal Auto Layout,Padding 配置为
Left: 12px, Right: 0, Top: 0, Bottom: 0; - 将右侧微调手柄放入末尾,左侧形成 1px 的纵向分割线。
2. 拓扑 B:两端分离加减型(Split Stepper Controls) #
常见于移动端 App、iPad 触控终端或电商加减购界面。用户主要使用手指点按,需要更大的物理点击热区(>= 36px 或 44px)。
+--------+------------------------------------+--------+
| [ - ] | [ 5 ] | [ + ] |
+--------+------------------------------------+--------+
Auto Layout 搭建步骤: #
- 外层容器:设为 Horizontal Auto Layout,对齐方式为居中对齐(Center Alignment);
- 两端独立按钮:
- 左侧为减号按钮(
Btn/Minus),右侧为加号按钮(Btn/Plus); - 宽高均设为正方形(例如
36px * 36px或40px * 40px); - 内部包裹减号与加号矢量图标,圆角统一配置(如
6px或全圆角999px);
- 左侧为减号按钮(
- 中间数值文本区:
- 宽度设置为 Fill container(或固定宽度
48px ~ 64px),文本对齐模式设置为 水平与垂直双向居中(Align Text Center); - 确保当用户连续点击两端加减号时,数字始终稳定固定在中心,不会偏左或偏右。
- 宽度设置为 Fill container(或固定宽度
深入排版布局可深度参考 Auto Layout 自动布局 与 对齐与分布技巧。
三、前缀/后缀单位加成(Prefix/Suffix/Unit)与等宽排印(Tabular Figures) #
数字输入框的专业度不仅取决于外形,更体现在细微的字体排印细节中。
1. 消除数字跳动的核心武器:OpenType 等宽数字(Tabular Figures) #
在常规排版中,字体采用的是 比例数字(Proportional Figures),每个数字的占位宽度根据字形曲线动态分配。例如在大多数无衬线字体中,数字 1 的宽度仅为数字 8 的 60%。
当步进器数值变化时,字符宽度的突变会引发整个数值文本图层宽度的抖动,进而导致光标与相邻元素剧烈跳动。
解决方案: #
- 选中数值文本图层;
- 打开右侧属性面板的 Text(文本) 区域,点击右下角三个点打开 Type details(排版高级选项);
- 向下滚动至 OpenType features,找到 Number style / Numbers 设置;
- 将默认的 Proportional Figures 切换为 Tabular Figures(等宽数字,Monospaced Numbers);
- 此时,所有数字
0到9将被强制赋予完全均等的水平字宽。无论数值如何加减递增,整体文本宽度纹丝不动,彻底消除视觉抖动!
2. 前缀(Prefix)与后缀(Suffix)插槽解耦 #
通过 Component Properties(组件属性) 解耦单位元素:
- 前置符号插槽(Prefix Slot):用于展示币种符号(
¥、$、€)或正负符号(±),颜色通常使用中性次级灰色,避免干扰核心数值的辨识度; - 后置度量单位(Suffix Slot):用于展示物理或业务计量单位(如
%、px、rem、kg、ms、台)。单位与数字之间应保持固定的微间距(如4px),且当输入框被禁用(Disabled)时,单位颜色必须随同淡化。
3. 步进器拓扑与参数规格对比表 #
| 拓扑模式 | 建议高度 | 推荐使用端 | 按钮布局机制 | 单位展示方式 | 典型核心业务场景 |
|---|---|---|---|---|---|
| 内嵌上下微调型 (Inline Stacked) | 32px / 40px | 桌面端 SaaS / 专业控制台 | 右侧纵向垂直双按钮 (Up/Down) | 内嵌 Suffix(如 px / %) | 设计器参数调节、报表筛选、阈值微调 |
| 两端分离加减型 (Split Buttons) | 36px / 44px | 移动端 App / 触控平板 | 左右两端独立大按钮 (- / +) | 外置 Label 或居中数值 | 移动端购物车加减、点餐份数选择 |
| 无按钮纯键盘型 (Keyboard Only) | 28px / 32px | 高密度金融数据表格 | 无常驻按钮,悬浮或按键展开 | 紧贴数字或 Tooltip | 大批量财务记账凭证、ERP 采购清单录入 |
| 滑块组合型 (Slider Linked) | 40px | 多媒体与音画调节软件 | 步进器与连续滑块双向绑定 | 动态悬浮标牌 | 亮度调节、音频增益、画质清晰度控制 |
排印规范与等宽字体设计可深入学习 排版变量落地实战 与 统计数值与指标卡。
四、极值边界约束(Min/Max/Step)与智能禁用态状态机 #
数字输入绝非无节制的自由递增。每个业务场景都存在严格的物理或逻辑上下界:
- 例如:购买商品数量下限
min = 1,库存上限max = 99; - 折扣率范围:
min = 0.01,max = 1.00,步长step = 0.05。
1. 边界状态正交矩阵表 #
| 当前数值状态 (Value State) | 减号按钮状态 (Decrement) | 加号按钮状态 (Increment) | 输入框边框与背景 | 交互行为预期 |
|---|---|---|---|---|
| 下界极值态 (Value <= Min) | Disabled(置灰禁用) | Active(可点击) | 标准默认边框 | 减号点击被拦截;加号可继续递增 |
| 合法区间态 (Min < Value < Max) | Active(可点击) | Active(可点击) | 标准默认边框 | 两端均可自由点击加减 |
| 上界极值态 (Value >= Max) | Active(可点击) | Disabled(置灰禁用) | 标准默认边框 | 加号点击被拦截;减号可继续递减 |
| 非法越界/报错态 (Out of Bound) | Disabled / Warning | Disabled / Warning | Error 红色边框高亮 | 显示校验错误提示气泡并自动回弹 |
2. 禁用态视觉与交互规范 #
在 Figma 组件库中:
- 为加减按钮配置独立变体属性:
DecrementState: Enabled | Disabled,IncrementState: Enabled | Disabled; - 当处于
Disabled状态时:- 按钮图标与背景不透明度统一降为
35% ~ 40%; - 鼠标悬浮指针切换为禁止光标(
cursor: not-allowed); - 移除任何悬浮微动效,防止给用户带来“可以继续点击”的误导心理。
- 按钮图标与背景不透明度统一降为
相关状态流转机制可参考 原子选择控件系统。
五、Variables 实时数学计算与加减原型状态机 #
通过 Figma 的 Variables(变量) 与 Conditional Logic(条件判断) 原型引擎,我们无需编写任何一行前端代码,就能在 Figma 原型演示中实现真实的数值连续加减与边界智能拦截!
+------------------------------------------------------------------------+
| Figma Variables 数值状态机闭环模型 |
| |
| [ 点击 '+' 按钮 ] |
| | |
| v |
| [ 判定: currentValue < max? ] |
| / \ |
| (YES) (NO) |
| v v |
| Set: currentValue += step 拦截点击并置灰加号 |
| 若新值 == max 则置灰加号 (isMaxDisabled = true) |
| 激活减号 (isMinDisabled=false) |
+------------------------------------------------------------------------+
1. 变量清单与类型定义 #
| 变量名称 | 类型 (Type) | 初始值 (Default) | 业务作用 |
|---|---|---|---|
currentValue | Number | 1 | 当前输入框呈现的核心量化数值 |
minValue | Number | 1 | 允许输入的下限最小值 |
maxValue | Number | 10 | 允许输入的上限最大值 |
stepValue | Number | 1 | 单次加减调整的步长 |
isMinDisabled | Boolean | true | 当 currentValue <= minValue 时为真 |
isMaxDisabled | Boolean | false | 当 currentValue >= maxValue 时为真 |
2. 交互连线与条件逻辑编排 #
- 为加号按钮(Btn/Plus 或 Btn/Up)配置 On click 交互:
- 选择 Add action -> Conditional(添加条件判断);
- 条件表达式输入:
if currentValue < maxValue; - 满足条件时执行动作(Then):
- 动作 1:
Set variable currentValue = currentValue + stepValue; - 动作 2:嵌套内层条件
if currentValue >= maxValue,则Set variable isMaxDisabled = true; - 动作 3:
Set variable isMinDisabled = false(确保原本可能置灰的减号重新点亮恢复可用);
- 动作 1:
- 为减号按钮(Btn/Minus 或 Btn/Down)配置 On click 交互:
- 添加条件判断:
if currentValue > minValue; - 满足条件时执行动作:
- 动作 1:
Set variable currentValue = currentValue - stepValue; - 动作 2:嵌套内层条件
if currentValue <= minValue,则Set variable isMinDisabled = true; - 动作 3:
Set variable isMaxDisabled = false;
- 动作 1:
- 添加条件判断:
- 将数值变量绑定到界面图层:
- 选中居中的数字文本图层,在右侧 Text 区域点击右下角 Apply variable 图标,将其精准绑定至
currentValue; - 将加减按钮的可见性或变体切换分别绑定至
isMaxDisabled与isMinDisabled。
- 选中居中的数字文本图层,在右侧 Text 区域点击右下角 Apply variable 图标,将其精准绑定至
当用户在演示模式下点击加号时,界面上的数字将即时从 1 连续跳动到 10,并在达到 10 时加号按钮瞬间置灰,完全具备生产级应用的交互真实度!
进阶变量与计算逻辑请参考 Figma 变量 Variables 实战 与 原型交互高级配置。
六、Dev Mode 现代前端工程交付:React + TypeScript + WAI-ARIA spinbutton 规范 #
在交付给研发团队时,前端工程师不仅需要获取视觉样式,更需要获得完备的无障碍键盘操作(如键盘上下箭头 ArrowUp / ArrowDown 步进)以及极值安全校验。
在 Dev Mode 开发者模式 下,数字输入框必须映射为符合 W3C 标准的 role="spinbutton" 语义结构。
1. 核心属性与键盘可访问性规范 #
- WAI-ARIA 语义规范:
- 容器声明
role="spinbutton"; - 动态绑定
aria-valuenow(当前值)、aria-valuemin(最小值)、aria-valuemax(最大值); - 当包含单位时,通过
aria-valuetext(例如aria-valuetext="5 份")辅助盲人阅读;
- 容器声明
- 键盘支持:
- 按下键盘
↑(ArrowUp):触发数值递增 1 个 step; - 按下键盘
↓(ArrowDown):触发数值递减 1 个 step; - 按下
PageUp/PageDown:可触发 10 倍大步长递增/递减。
- 按下键盘
2. 生产级 React + TypeScript + Tailwind 规范实现 #
import React, { useState, useEffect, useRef, KeyboardEvent } from 'react';
import { ChevronUp, ChevronDown, Minus, Plus } from 'lucide-react';
export interface InputNumberProps {
value?: number;
defaultValue?: number;
min?: number;
max?: number;
step?: number;
precision?: number;
prefix?: string;
suffix?: string;
size?: 'sm' | 'md' | 'lg';
variant?: 'stacked' | 'split';
disabled?: boolean;
onChange?: (val: number) => void;
className?: string;
}
export const EnterpriseInputNumber: React.FC<InputNumberProps> = ({
value: controlledValue,
defaultValue = 1,
min = 0,
max = 100,
step = 1,
precision = 0,
prefix,
suffix,
size = 'md',
variant = 'stacked',
disabled = false,
onChange,
className = '',
}) => {
const [internalValue, setInternalValue] = useState<number>(
controlledValue !== undefined ? controlledValue : defaultValue
);
const currentValue = controlledValue !== undefined ? controlledValue : internalValue;
const updateValue = (newValue: number) => {
if (disabled) return;
// 精度格式化与极值拦截
const clamped = Math.min(max, Math.max(min, newValue));
const factor = Math.pow(10, precision);
const rounded = Math.round(clamped * factor) / factor;
setInternalValue(rounded);
onChange?.(rounded);
};
const isMinDisabled = disabled || currentValue <= min;
const isMaxDisabled = disabled || currentValue >= max;
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (disabled) return;
if (e.key === 'ArrowUp') {
e.preventDefault();
updateValue(currentValue + step);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
updateValue(currentValue - step);
}
};
// 尺寸梯度配置
const sizeClasses = {
sm: 'h-8 text-xs',
md: 'h-10 text-sm',
lg: 'h-12 text-base',
}[size];
// 拓扑 B:两端分离加减型 (Split Buttons)
if (variant === 'split') {
return (
<div className={`inline-flex items-center gap-2 ${className}`}>
<button
type="button"
disabled={isMinDisabled}
aria-label="减少数值"
onClick={() => updateValue(currentValue - step)}
className="w-9 h-9 flex items-center justify-center rounded-lg border border-slate-300 bg-white text-slate-700 hover:bg-slate-50 active:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed transition-all shadow-sm"
>
<Minus className="w-4 h-4" />
</button>
<div className="relative flex items-center justify-center min-w-[64px]">
<span className="font-mono text-center font-semibold text-slate-900 select-none">
{prefix && <span className="text-slate-400 mr-1">{prefix}</span>}
{currentValue.toFixed(precision)}
{suffix && <span className="text-slate-400 ml-1 text-xs">{suffix}</span>}
</span>
</div>
<button
type="button"
disabled={isMaxDisabled}
aria-label="增加数值"
onClick={() => updateValue(currentValue + step)}
className="w-9 h-9 flex items-center justify-center rounded-lg border border-slate-300 bg-white text-slate-700 hover:bg-slate-50 active:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed transition-all shadow-sm"
>
<Plus className="w-4 h-4" />
</button>
</div>
);
}
// 拓扑 A:内嵌紧凑微调型 (Inline Stacked Controls)
return (
<div
className={`relative inline-flex items-center rounded-lg border border-slate-300 bg-white focus-within:border-blue-600 focus-within:ring-2 focus-within:ring-blue-100 transition-all shadow-sm ${sizeClasses} ${
disabled ? 'bg-slate-100 opacity-60 cursor-not-allowed' : ''
} ${className}`}
>
{prefix && (
<span className="pl-3 pr-1 text-slate-400 font-medium select-none">
{prefix}
</span>
)}
{/* 数值输入区域:启用 font-mono 等宽排印抗抖动 */}
<input
type="text"
role="spinbutton"
aria-valuenow={currentValue}
aria-valuemin={min}
aria-valuemax={max}
disabled={disabled}
value={currentValue.toFixed(precision)}
onChange={(e) => {
const val = parseFloat(e.target.value);
if (!isNaN(val)) updateValue(val);
}}
onKeyDown={handleKeyDown}
className="w-full h-full px-3 py-1 bg-transparent font-mono text-slate-900 font-medium border-none outline-none focus:ring-0"
/>
{suffix && (
<span className="pr-2 text-slate-400 text-xs font-normal select-none">
{suffix}
</span>
)}
{/* 右侧微调垂直堆叠按钮 */}
<div className="flex flex-col h-full w-6 border-l border-slate-200 divide-y divide-slate-200">
<button
type="button"
tabIndex={-1}
disabled={isMaxDisabled}
aria-label="向上递增"
onClick={() => updateValue(currentValue + step)}
className="flex-1 flex items-center justify-center hover:bg-slate-100 text-slate-500 hover:text-slate-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronUp className="w-3.5 h-3.5" />
</button>
<button
type="button"
tabIndex={-1}
disabled={isMinDisabled}
aria-label="向下递减"
onClick={() => updateValue(currentValue - step)}
className="flex-1 flex items-center justify-center hover:bg-slate-100 text-slate-500 hover:text-slate-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronDown className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
};
有关 Dev Mode 前端工程代码导出与组件封装规范,可深度参考 Dev Mode 开发者模式、Code Connect 企业落地指南 以及 无障碍设计 a11y 合规落地实战。
核心手册与速查工具
高频手册与实用设计生产力工具直达常见问题与答疑 (FAQ)
高频疑难解析与避坑指南完全不需要。绝大多数核心功能在免费 Starter 计划中均可完整体验与实操练习,无需额外付费购买付费团队版。