在表单设计中,我们经常需要让用户输入一个数值,同时旁边固定显示单位,比如时间间隔的分钟数“min”、重量的“kg”或长度的“cm”。理想状态下,单位应该紧贴在数字右侧,看起来像是输入框的一部分,但用户的光标只能进入数字区域,单位不可编辑。如果简单地把单位和数字一起放在value里,用户可能误删或修改单位;如果使用placeholder,占位符在输入后会消失,无法常驻。因此需要结合布局和输入控制来实现。

方案一:外层容器与单位标签组合
最直接的方式是放弃使用单个<input>,将输入框和单位拆成两个元素,再用一个容器把它们包起来,通过样式让容器看起来像一个完整的输入框。HTML结构可以是一个<div>容器,里面放一个<input>和一个<span>,<span>用来显示单位。输入框去掉边框,容器设置边框和背景,利用flex布局让输入框占据剩余空间,单位贴在右侧。这样单位天然不可编辑,因为它是普通文本节点,用户无法把光标移入。
这种方案的CSS实现并不复杂。给容器设置display: flex和align-items: center,输入框设置flex: 1并移除边框和轮廓,单位标签设置右侧间距和浅色。当输入框获得焦点时,通过:focus-within伪类改变容器边框颜色,模拟原生输入框的聚焦效果。示例代码见下:
<div class="input-with-unit">
<input type="text" class="unit-input" placeholder="请输入分钟数" />
<span class="unit-label">min</span>
</div>
<style>
.input-with-unit {
display: flex;
align-items: center;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0 8px;
background: #fff;
width: 220px;
}
.unit-input {
flex: 1;
border: none;
outline: none;
padding: 8px 0;
font-size: 14px;
min-width: 0;
}
.unit-label {
margin-left: 8px;
color: #666;
font-size: 14px;
user-select: none;
}
.input-with-unit:focus-within {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64,158,255,0.2);
}
</style>
这个方案的优点是结构清晰、兼容性好,不依赖伪元素或复杂定位,单位不会被输入法或光标选中。缺点是需要额外的<span>元素,如果要做成可复用组件,需要维护两个节点。另外移动端点击单位区域不会自动聚焦到输入框,可以给容器添加点击事件让输入框获取焦点。在无障碍方面,可以让<span>设置aria-hidden="true",避免屏幕阅读器重复朗读单位,同时在<input>上使用aria-label说明单位含义。
方案二:利用CSS伪元素把单位放入输入框内部
由于<input>元素本身不支持::after或::before伪元素,要实现单位出现在输入框内部,需要在<input>外面包一层<div>,在<div>上使用::after伪元素生成单位文本,并通过定位让它显示在输入框右侧内部。同时给输入框设置足够的padding-right,避免输入的长数字被单位遮挡。这种方案不需要额外的<span>元素,HTML结构更简洁。
实现时,外层容器设置position: relative,伪元素设置position: absolute并定位到右侧垂直居中,使用pointer-events: none禁止鼠标事件,user-select: none防止选中。输入框的右内边距至少要比单位文本宽度多一些。示例代码:
<div class="unit-field">
<input type="text" class="unit-field-input" placeholder="输入数值" />
</div>
<style>
.unit-field {
position: relative;
width: 220px;
}
.unit-field::after {
content: "min";
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 14px;
pointer-events: none;
user-select: none;
}
.unit-field-input {
width: 100%;
padding: 8px 48px 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.unit-field-input:focus {
border-color: #409eff;
outline: none;
}
</style>
这个方案最大的优势是单位完全由CSS生成,不参与DOM文本流,用户无法选中或复制,适合单位固定且不需要动态变化的场景。但伪元素内容不能通过JS直接修改,如果单位需要跟随配置变化,需要定义多个类或使用CSS变量结合content属性,灵活性略差。另外定位依赖固定宽度和padding,当输入框宽度变化或单位字符长度变化时,需要重新调整。对于响应式设计,可以结合em单位或calc函数,但维护成本稍高。
方案三:用JavaScript过滤非数字输入
前两种方案主要解决单位的显示问题,但用户仍然可以在输入框中输入字母、符号等非数字内容。要严格限制只能输入数字,需要在输入过程中进行过滤。常见做法是监听input事件,在每次输入后检查value,移除不符合规则的字符,并恢复光标位置。也可以提前监听keydown阻止特定按键,但keydown无法拦截粘贴、拖放和输入法,因此推荐以input事件为主。
下面的示例实现一个只允许非负整数的输入框,同时兼容粘贴和输入法。代码中使用了input事件,先保存当前光标位置,再过滤掉所有非数字字符,最后把处理后的值写回,并恢复光标到合理位置。需要注意的是直接设置input.value会导致光标跳到末尾,因此必须手动恢复。示例:
const input = document.querySelector('.unit-input');
input.addEventListener('input', function (e) {
const rawValue = this.value;
// 只保留数字字符
const filtered = rawValue.replace(/\D/g, '');
if (filtered !== rawValue) {
const cursorPos = this.selectionStart;
// 计算过滤后光标位置:原始光标减去被删除的非数字个数(在光标之前)
const removedBefore = rawValue.slice(0, cursorPos).replace(/\D/g, '').length;
this.value = filtered;
// 将光标放到过滤后对应的位置
const newCursor = Math.min(removedBefore, filtered.length);
this.setSelectionRange(newCursor, newCursor);
}
});
如果允许小数或负数,可以调整正则表达式。例如允许小数点和负号可以使用replace(/[^0-9.-]/g, ''),但还需要进一步校验负号只能出现在开头,小数点只能出现一次。对于更复杂的规则,建议结合表单验证库或使用原生pattern属性做兜底。移动端体验上,可以给输入框设置inputmode="numeric"或inputmode="decimal",这样在iOS和Android上会优先唤起数字键盘。注意不要使用type="number",因为它的原生增减按钮和本地化显示可能与自定义单位冲突。
方案四:无障碍与整体交互优化
实现基本功能后,还需要考虑无障碍和用户体验。对于屏幕阅读器用户,单位<span>或伪元素可能会被单独朗读,造成困扰。可以在单位元素上添加aria-hidden="true",同时在输入框上使用aria-label描述整个输入。如果使用外层容器方案,点击单位区域时应让输入框获得焦点,可以给容器添加点击事件判断目标是否为<span>,然后调用输入框的focus()方法。
另一个细节是输入框的readonly或disabled状态要与单位视觉一致。当输入框不可用时,单位颜色应同步变浅,而不是保持原样。此外如果输入框支持清空,需要明确清空后单位仍然显示。在移动端,输入框字体大小不应小于16px,否则iOS会在聚焦时自动缩放页面。这些细节会直接影响最终体验。
容器点击聚焦的简单实现:
const wrapper = document.querySelector('.input-with-unit');
const input = wrapper.querySelector('input');
wrapper.addEventListener('click', function (e) {
if (e.target !== input) {
input.focus();
}
});
最后总结一下,选择哪种方案取决于具体场景:如果项目组件化程度高且单位固定,伪元素方案最简洁;如果需要动态修改单位或追求更强的可访问性,外层容器方案更合适。JavaScript过滤则是数字输入控制的底线,无论哪种视觉方案都应配合使用。将这些思路组合起来,就能构建出一个稳定、易用的单位输入框。