OTPInput(验证码输入)用于登录、验证这类短验证码。每个字符占一格,粘贴或系统自动填充的整段验证码会一次填满所有格子。它底层只有一个原生输入,因此选区、撤销、自动填充与表单提交都是浏览器原生行为。普通短文本用输入框。
组合
OTPInput
└── OTPInputGroup
├── OTPInputSlot
└── OTPInputSlotOTPInput 创建原生输入并持有验证码;OTPInputGroup 排列一组格子;每个 OTPInputSlot 显示 index(从 0 开始)位置上的字符。分段更易读时,在两组之间插入 OTPInputSeparator。
分组与分隔符只改变同一个值的呈现,不会增加字段或 Tab 停留点。原生输入带 autocomplete="one-time-code",支持的浏览器与密码管理器可以直接提供收到或保存的验证码。
用法
import { OTPInput, OTPInputGroup, OTPInputSeparator, OTPInputSlot } from "@ui-sdk/react/otp-input";
function Example() {
return (
<OTPInput maxLength={6} aria-label="验证码">
<OTPInputGroup>
{[0, 1, 2].map((index) => <OTPInputSlot key={index} index={index} />)}
</OTPInputGroup>
<OTPInputSeparator aria-hidden="true">-</OTPInputSeparator>
<OTPInputGroup>
{[3, 4, 5].map((index) => <OTPInputSlot key={index} index={index} />)}
</OTPInputGroup>
</OTPInput>
);
}输入六位数字,或直接粘贴 123-456:分隔符会被剔除,六个字符依次落格。点击任意格子可改写该位。
长度、字符与键盘
maxLength 是验证码长度,默认 6,需要渲染同样数量的格子。pattern 决定接受哪些字符,取 "digits"(默认)、"letters"、"alphanumeric",或一个逐字符匹配的正则;不符合的输入、粘贴、拖放内容会被忽略。
<OTPInput maxLength={6} pattern={/[0-9A-F]/i} inputMode="text">
<OTPInputGroup>
{[0, 1, 2, 3, 4, 5].map((index) => <OTPInputSlot key={index} index={index} />)}
</OTPInputGroup>
</OTPInput>上例只收十六进制字符:输入 12zzab 得到 12ab。inputMode 单独决定触屏键盘("decimal"、"numeric"、"tel"、"text"、"none"),预设 pattern 会自动选择合适的键盘。textAlign 决定隐藏输入把文本与光标放在哪里("start"、"center"、"end"),这会影响手机粘贴菜单等浏览器自带浮层的位置。
填满即提交
onValueCommit 在一次变更填满所有格子时触发,输入、粘贴、自动填充、控制器写入都算;已填满后替换字符会再次触发,表单重置与浏览器状态恢复不会。适合在这里直接发起校验请求。
<OTPInput maxLength={6} aria-label="验证码" onValueCommit={(value, details) => verify(value, details.reason)}>
<OTPInputGroup>
{[0, 1, 2, 3, 4, 5].map((index) => <OTPInputSlot key={index} index={index} />)}
</OTPInputGroup>
</OTPInput>受控与控制器
const [value, setValue] = React.useState("");
<OTPInput value={value} onValueChange={setValue}>
…
</OTPInput>;传入的值同样会被 pattern 过滤并截断到 maxLength。details.reason 区分 input-change(键入、自动填充、键盘候选)、paste、undo / redo、reset、restore 与 imperative;details.cancel() 保留原值。参见状态与事件。
用 createOTPInputController() 创建控制器传给根,可在组件树外调用 getValue / setValue / clear / focus;写入方法接受 { reason, event },被取消或值未变化时返回 false。参见控制器。
表单与校验
给根传 name,验证码进入 FormData。required 阻止空值提交,输入到一半的验证码按长度校验失败并报 tooShort;默认值或控制器写入的不完整值走原生输入的 pattern 校验,因为浏览器不对这类值应用 minlength。form 可把表单外的控件关联到某个表单 id,重置回到 defaultValue。
服务端拒绝验证码时,把字段标为 invalid,它会为输入加上 aria-invalid 并关联 FieldError:
<Field invalid={invalid}>
<FieldLabel>验证码</FieldLabel>
<OTPInput defaultValue="482913" onValueChange={() => setInvalid(false)}>
<OTPInputGroup>
{[0, 1, 2, 3, 4, 5].map((index) => <OTPInputSlot key={index} index={index} />)}
</OTPInputGroup>
</OTPInput>
<FieldError>验证码已过期,请重新获取。</FieldError>
</Field>不在字段内时,自己传 aria-invalid 与 aria-describedby。disabled 不可聚焦也不提交,readOnly 可聚焦、可提交但不能改。参见表单。
样式与状态
原生输入透明覆盖在格子之上,格子的外观全靠状态属性:
- 根与分组:
data-filled(已有字符)、data-complete(全部填满)、data-focused、data-disabled。 - 格子:
data-filled、data-active(当前键入落点,仅聚焦时)、data-fake-caret(当前格为空、需要自绘光标,真实光标已隐藏)、data-disabled。 - 分隔符:
data-disabled。
每个格子保持 position: relative,自绘光标才不会跑出去:
[data-slot="otp-input-slot"] {
position: relative;
width: 2.5rem;
height: 3rem;
border: 1px solid var(--color-border);
border-radius: 8px;
}
[data-slot="otp-input-slot"][data-active] {
border-color: var(--color-brand);
}
[data-slot="otp-input-slot"][data-fake-caret]::after {
content: "";
position: absolute;
inset-block: 25%;
inset-inline-start: 50%;
width: 1px;
background: currentColor;
animation: otp-caret 1s steps(1) infinite;
}
@keyframes otp-caret {
50% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
[data-slot="otp-input-slot"][data-fake-caret]::after {
animation: none;
}
}需要完全自绘时,根的状态里有 slots 数组,每格的状态提供 char、active、hasFakeCaret、index,参见部件与组合。
API
OTPInput 常用属性(id、autoFocus、aria-label、aria-labelledby、aria-describedby、aria-invalid 直接落到原生输入上):
| 属性 | 类型 | 说明 |
|---|---|---|
value / defaultValue |
string |
受控值与非受控初始值。 |
onValueChange |
(value, details) => void |
值变化时触发,可 details.cancel()。 |
onValueCommit |
(value, details) => void |
变更填满所有格子时触发,收到完整验证码。 |
maxLength |
number |
验证码长度,默认 6。 |
pattern |
"digits" | "letters" | "alphanumeric" | RegExp |
逐字符过滤规则。 |
inputMode |
"decimal" | "numeric" | "tel" | "text" | "none" |
触屏键盘类型。 |
textAlign |
"start" | "center" | "end" |
隐藏输入的文本与光标位置。 |
autoComplete |
string |
原生 autocomplete,默认支持 "one-time-code"。 |
name / form |
string |
提交字段名与关联表单 id。 |
disabled / readOnly / required |
boolean |
禁用、只读、必填。 |
controller |
object |
createOTPInputController() 创建的控制器。 |
OTPInputSlot 只有一个必填属性 index(number),表示它显示第几个字符。
键盘与无障碍
用 FieldLabel、原生 label、aria-label 或 aria-labelledby 为输入命名。屏幕阅读器只会遇到一个包含完整验证码的输入,格子、分组与分隔符对它隐藏。
输入保留原生按键:方向键、Home、End 在格子间移动选区,Backspace 删除字符,系统撤销与重做快捷键照常可用,粘贴 123-456 这类带分隔符的文本会自动清理。填满后再输入会替换当前选中的格子。