验证码输入
由若干独立字符格子组成的一次性密码输入框。
安装
pnpm dlx shadcn@latest add @lumi-ui/otp-field
import { OTPField } from "@/components/ui/otp-field"<OTPField length={6} />组件结构
<OTPField length={6} />尺寸
字母数字混合
对于恢复码、备份码或邀请码这类字母数字混合的场景,可以使用 validationType="alphanumeric"。完整枚举见 校验类型。
分组显示
当你希望像 123-456 这样把验证码以小块的形式呈现时,可以将子集输入框包裹在你自己的布局元素中,并搭配 <OTPFieldSeparator> 使用。这需要使用基础组件 —— 复合组件只会渲染输入框。
遮罩显示
使用 mask 属性可以遮罩所有格子,或者向单个 <OTPFieldInput> 传入 type="password",实现按格子级别的精细控制。
<OTPFieldRoot length={6}>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput type="password" />
<OTPFieldInput type="password" />
<OTPFieldInput type="password" />
<OTPFieldInput type="password" />
</OTPFieldRoot>占位符提示
<OTPFieldInput> 是一个真正的 input,所以原生的 placeholder 属性和 CSS 都能照常使用。下面这个示例会一直显示占位符提示,直到当前活动的格子获得焦点为止。
无障碍
每个 OTP 输入框都必须有可访问的名称。在下面两种模式中任选其一即可。
使用原生 label
把 id 传给 <OTPFieldRoot>,再用 <label htmlFor> 与之关联。第一个格子会自动接收这个 label;其余格子加上 aria-label,这样屏幕阅读器才能播报当前聚焦的是第几个字符。辅助说明文字使用 aria-describedby 关联。
<label htmlFor="verification-code">验证码</label>
<OTPFieldRoot
id="verification-code"
length={6}
aria-describedby="verification-code-help"
>
<OTPFieldInput />
<OTPFieldInput aria-label="第 2 位,共 6 位" />
<OTPFieldInput aria-label="第 3 位,共 6 位" />
<OTPFieldInput aria-label="第 4 位,共 6 位" />
<OTPFieldInput aria-label="第 5 位,共 6 位" />
<OTPFieldInput aria-label="第 6 位,共 6 位" />
</OTPFieldRoot>
<p id="verification-code-help">
请输入我们发送到你设备的 6 位验证码。
</p>复合组件 <OTPField> 已经为每个格子自动加上了 aria-label,使用时只需提供 id 和 <label> 即可。
配合 Field 使用
使用 <Field> 可以自动处理标签关联、描述和表单校验:
<Field name="verificationCode">
<FieldLabel>验证码</FieldLabel>
<FieldDescription>
请输入我们发送到你设备的 6 位验证码。
</FieldDescription>
<OTPField length={6} required />
<FieldError />
</Field>自定义清洗逻辑
将 validationType 设为 "none" 并搭配 sanitizeValue,可以在数值进入 state 之前对粘贴内容进行规范化。如果自定义规则仍需要特定的虚拟键盘提示,使用 inputMode 指定;用 onValueInvalid 来响应被拒绝的字符。下面这个示例只接受数字 0-3,被拒绝时会让聚焦格子抖动,并通过 aria-live 播报被丢弃的内容。
当 validationType 为 'numeric'、'alpha' 或 'alphanumeric' 时,sanitizeValue 会被静默忽略 —— 内置的清洗逻辑会优先生效。如果想在内置规则之上加入自定义逻辑,请将 validationType 设为 "none",并在自己的清洗函数中重新实现该规则。
为了开箱即用,演示中通过 <style> 标签内联了 keyframes。在生产环境下,建议把 @keyframes otp-shake-a/b 和 .otp-shake-a/b 规则提到全局 CSS(或 Tailwind 插件)中,并移除 <style> 元素。
与 Form 配合
传入 autoSubmit,会在所有格子填满后自动提交所在表单;或者使用 onValueComplete,在不提交表单的情况下响应填写完成事件。回调顺序详见 Complete 与 autoSubmit。
更多信息请参阅 表单集成。
原生表单提交
提交的表单值是一个长度为 length 的字符串,键名为 <OTPFieldRoot> 上的 name:
<form action="/verify" method="post">
<OTPField length={6} name="code" required />
</form>
// POST body: { code: "123456" }搭配 autoSubmit 可以完全省去显式的提交按钮。对于客户端表单库(React Hook Form、Conform 等),可用 value + onValueChange 驱动字段,并以 value.length === length 作为校验条件。
受控与非受控
<OTPField defaultValue="123" length={6} onValueComplete={handleComplete} />const [value, setValue] = useState("");
<OTPField length={6} value={value} onValueChange={setValue} />value 和 defaultValue 互斥。值始终是一个长度不超过 length 的字符串 —— 永远不是数组。只有当字段填写完成时,value.length 才会等于 length。
Complete 与 autoSubmit
每次按键或粘贴使字段被填满时,回调的触发顺序为:
onValueChange(value, { reason })—— 在输入、粘贴、清空或导致值改变的键盘导航时触发。onValueComplete(value, { reason })—— 当value.length === length时触发一次,且在状态更新完成之后。reason可能是'input-change' | 'input-paste'。- 表单提交 —— 仅当设置了
autoSubmit时才会触发,紧随onValueComplete之后执行。
无论是否启用 autoSubmit,onValueComplete 都会触发 —— 你可以用它来推进向导步骤、调用校验 API,或者在不提交表单的情况下关闭对话框。
API 参考
组件
OTPFieldRoot 属性
OTPFieldInput 属性
渲染一个 <input> 元素。接受所有原生 input 属性(type、placeholder、aria-label、onFocus 等),以及 Base UI 的渲染属性三件套:className、style、render。
本组件库的包装层还额外提供了一个 inputSize 变体:'sm' | 'default' | 'lg'。
校验类型
状态 data 属性
<OTPFieldRoot> 暴露以下属性:
<OTPFieldInput> 在每个格子上暴露 data-filled,同时也镜像了 Root 上的全部属性,方便样式控制。