OTP输入框通常用于短信验证码、邮箱验证码等场景,一般由多个独立的输入框组成,用户输入一位数字后焦点自动跳转到下一个输入框,删除内容时焦点自动回退,同时支持粘贴完整验证码自动填充所有输入框。在React中实现这类组件,核心在于处理好输入事件和焦点状态的切换逻辑。

基础结构设计
首先我们需要定义OTP输入框的基础结构,假设我们需要一个6位的OTP输入框,每个输入框只允许输入单个数字。我们可以先定义组件的状态和输入框的引用数组,用于后续操作焦点。
import React, { useRef, useState, useEffect } from 'react';
const OtpInput = ({ length = 6, onComplete }) => {
// 存储每个输入框的值
const [otp, setOtp] = useState(Array(length).fill(''));
// 存储每个输入框的ref
const inputRefs = useRef([]);
// 初始化ref数组
useEffect(() => {
inputRefs.current = inputRefs.current.slice(0, length);
}, [length]);
return (
<div className="otp-container">
{Array.from({ length }, (_, index) => (
<input
key={index}
type="text"
maxLength={1}
value={otp[index]}
ref={(el) => (inputRefs.current[index] = el)}
className="otp-input"
/>
))}
</div>
);
};
export default OtpInput;
输入事件处理
输入事件需要处理用户输入单个字符的场景,当我们在一个输入框中输入内容后,需要将焦点自动切换到下一个输入框。同时需要校验输入内容是否为数字,非数字内容不录入。
const handleChange = (e, index) => {
const value = e.target.value;
// 只允许输入数字
if (!/^d*$/.test(value)) return;
// 更新对应位置的值
const newOtp = [...otp];
// 取最后一位输入的内容,避免粘贴多个字符时只取第一个
newOtp[index] = value.slice(-1);
setOtp(newOtp);
// 如果输入了内容且不是最后一个输入框,焦点跳转到下一个
if (value && index < length - 1) {
inputRefs.current[index + 1].focus();
}
// 判断是否所有输入框都已填满,触发完成回调
if (newOtp.every((val) => val !== '')) {
onComplete?.(newOtp.join(''));
}
};
将handleChange函数绑定到每个输入框的onChange事件上即可实现输入后自动跳转焦点的效果。
键盘事件处理
除了输入事件,还需要处理键盘的删除键和左右方向键。当用户按下退格键且当前输入框为空时,焦点需要回退到上一个输入框;按下左方向键时焦点跳转到上一个输入框,右方向键跳转到下一个输入框。
const handleKeyDown = (e, index) => {
const { key } = e;
// 处理退格键
if (key === 'Backspace') {
// 当前输入框为空,且不是第一个输入框,回退焦点到上一个
if (!otp[index] && index > 0) {
inputRefs.current[index - 1].focus();
}
}
// 处理左方向键
if (key === 'ArrowLeft' && index > 0) {
inputRefs.current[index - 1].focus();
}
// 处理右方向键
if (key === 'ArrowRight' && index < length - 1) {
inputRefs.current[index + 1].focus();
}
};
粘贴事件处理
用户经常会复制验证码后直接粘贴到OTP输入框中,这时候需要解析粘贴的内容,自动填充到所有的输入框中。我们需要监听输入框的onPaste事件,解析粘贴的文本内容。
const handlePaste = (e) => {
e.preventDefault();
const pasteData = e.clipboardData.getData('text/plain').trim();
// 过滤非数字内容,只保留数字
const pasteNumbers = pasteData.replace(/D/g, '').slice(0, length);
if (!pasteNumbers) return;
const newOtp = [...otp];
pasteNumbers.split('').forEach((num, idx) => {
if (idx < length) {
newOtp[idx] = num;
}
});
setOtp(newOtp);
// 粘贴后焦点跳转到最后一个有值的输入框的下一个,或者最后一个输入框
const focusIndex = Math.min(pasteNumbers.length, length - 1);
inputRefs.current[focusIndex].focus();
// 判断是否所有输入框都已填满
if (newOtp.every((val) => val !== '')) {
onComplete?.(newOtp.join(''));
}
};
可以将handlePaste绑定到第一个输入框或者所有输入框的onPaste事件上,这里选择绑定到每个输入框,方便用户在任何输入框粘贴都能触发解析逻辑。
完整组件代码
将上述所有逻辑整合后,完整的OTP输入框组件代码如下:
import React, { useRef, useState, useEffect } from 'react';
import './OtpInput.css'; // 引入样式文件
const OtpInput = ({ length = 6, onComplete }) => {
const [otp, setOtp] = useState(Array(length).fill(''));
const inputRefs = useRef([]);
useEffect(() => {
inputRefs.current = inputRefs.current.slice(0, length);
// 初始时聚焦第一个输入框
if (inputRefs.current[0]) {
inputRefs.current[0].focus();
}
}, [length]);
const handleChange = (e, index) => {
const value = e.target.value;
if (!/^d*$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value.slice(-1);
setOtp(newOtp);
if (value && index < length - 1) {
inputRefs.current[index + 1].focus();
}
if (newOtp.every((val) => val !== '')) {
onComplete?.(newOtp.join(''));
}
};
const handleKeyDown = (e, index) => {
const { key } = e;
if (key === 'Backspace') {
if (!otp[index] && index > 0) {
inputRefs.current[index - 1].focus();
}
}
if (key === 'ArrowLeft' && index > 0) {
inputRefs.current[index - 1].focus();
}
if (key === 'ArrowRight' && index < length - 1) {
inputRefs.current[index + 1].focus();
}
};
const handlePaste = (e) => {
e.preventDefault();
const pasteData = e.clipboardData.getData('text/plain').trim();
const pasteNumbers = pasteData.replace(/D/g, '').slice(0, length);
if (!pasteNumbers) return;
const newOtp = [...otp];
pasteNumbers.split('').forEach((num, idx) => {
if (idx < length) {
newOtp[idx] = num;
}
});
setOtp(newOtp);
const focusIndex = Math.min(pasteNumbers.length, length - 1);
inputRefs.current[focusIndex].focus();
if (newOtp.every((val) => val !== '')) {
onComplete?.(newOtp.join(''));
}
};
return (
<div className="otp-container">
{Array.from({ length }, (_, index) => (
<input
key={index}
type="text"
maxLength={1}
value={otp[index]}
ref={(el) => (inputRefs.current[index] = el)}
onChange={(e) => handleChange(e, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
onPaste={handlePaste}
className="otp-input"
/>
))}
</div>
);
};
export default OtpInput;
基础样式示例
为了让OTP输入框看起来更美观,可以添加如下基础样式:
.otp-container {
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
}
.otp-input {
width: 40px;
height: 40px;
text-align: center;
font-size: 18px;
border: 1px solid #ccc;
border-radius: 4px;
outline: none;
}
.otp-input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2);
}
使用方式
在业务组件中引入OtpInput组件,传入length和onComplete回调即可使用:
import React from 'react';
import OtpInput from './OtpInput';
const App = () => {
const handleOtpComplete = (otp) => {
console.log('输入的验证码是:', otp);
// 这里可以调用后端接口验证验证码
};
return (
<div className="app">
<h3>请输入6位验证码</h3>
<OtpInput length={6} onComplete={handleOtpComplete} />
</div>
);
};
export default App;
注意事项
- 输入框的
type可以设置为number,但部分浏览器会显示上下调节箭头,更推荐用text配合正则校验限制输入内容。 - 移动端输入时,可以设置
inputMode为numeric,调起数字键盘,提升用户输入体验。 - 如果需要支持验证码自动填充(如短信验证码自动填充),可以结合
useEffect监听系统填充的验证码内容,自动更新输入框状态。