InputOTP 一次性密码输入
用于验证码与安全认证的一次性密码输入组件
用法
import { InputOTP } from '@lenso/ui';此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { InputOTP, Label, Link, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { Slots, styles } from "./parts";export function Basic() { return ( <TextField name="code" xstyle={styles.field}> <div {...stylex.props(styles.heading)}> <Label>Verify account</Label> <p {...stylex.props(styles.muted)}>We've sent a code to a****@gmail.com</p> </div> <InputOTP length={6} name="code"> <Slots /> </InputOTP> <div {...stylex.props(styles.resend)}> <p {...stylex.props(styles.muted)}>Didn't receive a code?</p> <Link xstyle={styles.link} href="#"> Resend </Link> </div> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
组件结构
import { InputOTP } from '@lenso/ui';
export default () => ( <InputOTP maxLength={6}> <InputOTP.Group> <InputOTP.Slot index={0} /> <InputOTP.Slot index={1} /> {/* ...rest of the slots */} </InputOTP.Group> <InputOTP.Separator /> <InputOTP.Group> <InputOTP.Slot index={3} /> {/* ...rest of the slots */} </InputOTP.Group> </InputOTP>)InputOTP 基于 @guilherme_rodz 的 input-otp 构建,为 OTP 输入组件提供灵活且无障碍的基础。
示例
变体
InputOTP 组件支持两种视觉变体:
primary(默认)- 标准样式带阴影,适用于大多数场景secondary- 低强调变体无阴影,适用于 Surface 组件内
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { InputOTP, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { Slots, styles } from "./parts";export function Variants() { return ( <div {...stylex.props(styles.variants)}> {(["primary", "secondary"] as const).map((variant) => ( <TextField key={variant} name={`${variant}-code`} xstyle={styles.field}> <Label>{variant === "primary" ? "Primary variant" : "Secondary variant"}</Label> <InputOTP length={6} name={`${variant}-code`} variant={variant}> <Slots /> </InputOTP> </TextField> ))} </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
表面样式
在 Surface 内使用时,请使用 variant="secondary" 以应用适合 Surface 背景的低强调变体。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { InputOTP, Label, Link, Surface, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { Slots, styles } from "./parts";export function OnSurface() { return ( <Surface xstyle={styles.surface}> <TextField name="code"> <div {...stylex.props(styles.heading)}> <Label>Verify account</Label> <p {...stylex.props(styles.muted)}>We've sent a code to a****@gmail.com</p> </div> <InputOTP length={6} name="code" variant="secondary"> <Slots /> </InputOTP> <div {...stylex.props(styles.resend)}> <p {...stylex.props(styles.muted)}>Didn't receive a code?</p> <Link xstyle={styles.link} href="#"> Resend </Link> </div> </TextField> </Surface> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
禁用状态
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, InputOTP, Label, TextField } from "@lenso/ui";import { Slots, styles } from "./parts";export function Disabled() { return ( <TextField disabled name="code" xstyle={styles.field}> <Label>Verify account</Label> <Description>Code verification is currently disabled</Description> <InputOTP disabled length={6} name="code"> <Slots /> </InputOTP> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
四位验证码
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { InputOTP, Label, TextField } from "@lenso/ui";import { styles } from "./parts";export function FourDigits() { return ( <TextField name="pin" xstyle={styles.field}> <Label>Enter PIN</Label> <InputOTP length={4} name="pin"> <InputOTP.Group> <InputOTP.Slot aria-label="Digit 1" /> <InputOTP.Slot aria-label="Digit 2" /> <InputOTP.Slot aria-label="Digit 3" /> <InputOTP.Slot aria-label="Digit 4" /> </InputOTP.Group> </InputOTP> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
受控组件
控制 value 以与状态同步、清空输入或实现自定义校验。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, InputOTP, Label, TextField } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { Slots, styles } from "./parts";export function Controlled() { const [value, setValue] = useState(""); return ( <TextField name="code" xstyle={styles.field}> <Label>Verify account</Label> <InputOTP length={6} name="code" value={value} onValueChange={setValue}> <Slots /> </InputOTP> <Description> {value.length > 0 ? ( <> Value: {value} ({value.length}/6) •{" "} <button type="button" {...stylex.props(styles.clear)} onClick={() => setValue("")}> Clear </button> </> ) : ( "Enter a 6-digit code" )} </Description> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
输入完成回调
使用 onComplete 回调在所有 slot 填满时触发操作。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Button, Form, InputOTP, Label, Spinner, TextField } from "@lenso/ui";import { useState, type FormEvent } from "react";import { Slots, styles } from "./parts";export function OnComplete() { const [value, setValue] = useState(""); const [isComplete, setIsComplete] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); if (!isComplete || isSubmitting) return; setIsSubmitting(true); setTimeout(() => { setIsSubmitting(false); setValue(""); setIsComplete(false); }, 2000); }; return ( <Form xstyle={styles.field} onSubmit={handleSubmit}> <TextField name="code"> <Label>Verify account</Label> <InputOTP length={6} name="code" value={value} onValueComplete={(code) => { setIsComplete(true); console.log("Code complete:", code); }} onValueChange={(next) => { setValue(next); setIsComplete(false); }} > <Slots /> </InputOTP> </TextField> <Button xstyle={styles.submit} disabled={!isComplete} isLoading={isSubmitting} type="submit" variant="primary" > {isSubmitting ? ( <> <Spinner color="current" size="sm" /> Verifying... </> ) : ( "Verify Code" )} </Button> </Form> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
表单示例
完整的双因素认证表单,含校验与提交。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Button, Description, FieldError, Form, InputOTP, Label, Link, Spinner, TextField,} from "@lenso/ui";import { useState, type FormEvent } from "react";import * as stylex from "@stylexjs/stylex";import { Slots, styles } from "./parts";export function FormExample() { const [value, setValue] = useState(""); const [error, setError] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); if (isSubmitting) return; setError(""); if (value.length !== 6) { setError("Please enter all 6 digits"); return; } setIsSubmitting(true); setTimeout(() => { if (value === "123456") { console.log("Code verified successfully!"); setValue(""); } else setError("Invalid code. Please try again."); setIsSubmitting(false); }, 1500); }; return ( <Form xstyle={styles.form} onSubmit={handleSubmit}> <TextField name="code" invalid={!!error}> <Label>Two-factor authentication</Label> <Description>Enter the 6-digit code from your authenticator app</Description> <InputOTP length={6} name="code" value={value} onValueChange={(next) => { setValue(next); setError(""); }} > <Slots /> </InputOTP> {error && <FieldError match>{error}</FieldError>} </TextField> <Button xstyle={styles.full} disabled={value.length !== 6} isLoading={isSubmitting} type="submit" variant="primary" > {isSubmitting ? ( <> <Spinner color="current" size="sm" /> Verifying... </> ) : ( "Verify" )} </Button> <div {...stylex.props(styles.help)}> <p {...stylex.props(styles.muted)}>Having trouble?</p> <Link xstyle={styles.link} href="#"> Use backup code </Link> </div> </Form> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
限定格式
使用 pattern prop 限制输入字符。HeroUI 导出 REGEXP_ONLY_CHARS、REGEXP_ONLY_DIGITS 等常用模式。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, InputOTP, Label, TextField } from "@lenso/ui";import { Slots, styles } from "./parts";export function WithPattern() { return ( <TextField name="code" xstyle={styles.field}> <Label>Enter code (letters only)</Label> <Description>Only alphabetic characters are allowed</Description> <InputOTP length={6} name="code" validationType="alpha"> <Slots /> </InputOTP> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
带校验
配合 isInvalid 与校验消息展示错误。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Button, Description, FieldError, Form, InputOTP, Label, TextField } from "@lenso/ui";import { useState, type FormEvent } from "react";import { Slots, styles } from "./parts";export function WithValidation() { const [value, setValue] = useState(""); const [isInvalid, setIsInvalid] = useState(false); const onSubmit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); const code = new FormData(event.currentTarget).get("code"); if (code !== "123456") { setIsInvalid(true); return; } setIsInvalid(false); setValue(""); alert("Code verified successfully!"); }; return ( <Form xstyle={styles.field} onSubmit={onSubmit}> <TextField name="code" invalid={isInvalid}> <Label>Verify account</Label> <Description>Hint: The code is 123456</Description> <InputOTP length={6} name="code" value={value} onValueChange={(next) => { setValue(next); setIsInvalid(false); }} > <Slots /> </InputOTP> {isInvalid && <FieldError match>Invalid code. Please try again.</FieldError>} </TextField> <Button disabled={value.length !== 6} type="submit"> Submit </Button> </Form> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义样式
Tailwind CSS
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { InputOTP, Label, Link, TextField } from "@lenso/ui";import { Slots, styles } from "./parts";export function CustomStyles() { return ( <TextField name="code" xstyle={[styles.field, styles.customWidth]}> <Label>Verify account</Label> <InputOTP length={6} name="code"> <Slots custom /> </InputOTP> <Link xstyle={styles.resendLink} href="#"> Resend code </Link> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
全局 CSS
可使用 @layer components 指令自定义 InputOTP 组件类。
了解更多。
@layer components { .input-otp { @apply gap-3; }
.input-otp__slot { @apply size-12 rounded-xl border-2 font-bold; }
.input-otp__slot[data-active="true"] { @apply border-accent-500 ring-2 ring-accent-200; }
.input-otp__separator { @apply w-2 h-1 bg-border-strong rounded-full; }}样式参考
HeroUI 遵循 BEM 方法论,确保组件变体与状态可复用且易于自定义。
CSS 类
InputOTP 组件使用以下 CSS 类(查看源码样式):
基础类 [!toc]
.input-otp- 基础容器.input-otp__container- input-otp 库的内部容器.input-otp__group- slot 组.input-otp__slot- 单个输入 slot.input-otp__slot-value- slot 内的字符.input-otp__caret- 闪烁光标指示器.input-otp__separator- 组之间的视觉分隔符
状态类 [!toc]
.input-otp__slot[data-active="true"]- 当前激活的 slot.input-otp__slot[data-filled="true"]- 含字符的 slot.input-otp__slot[data-disabled="true"]- 禁用的 slot.input-otp__slot[data-invalid="true"]- 无效的 slot.input-otp__container[data-disabled="true"]- 禁用的容器
交互状态
组件同时支持 CSS 伪类与 data 属性:
- Hover:slot 上
:hover或[data-hovered="true"] - Active:slot 上
[data-active="true"](当前聚焦) - Filled:slot 上
[data-filled="true"](含字符) - Disabled:容器与 slot 上
[data-disabled="true"] - Invalid:slot 上
[data-invalid="true"]
API 参考
InputOTP
InputOTP 基于 input-otp 库构建,并附加额外特性。
Base Props
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
maxLength | number | - | 必填。 输入 slot 数量 |
value | string | - | 受控值(未提供则为非受控) |
onChange | (value: string) => void | - | 值变化时的回调 |
onComplete | (value: string) => void | - | 所有 slot 填满时的回调 |
className | string | - | 容器的附加 CSS 类 |
containerClassName | string | - | 内部容器的 CSS 类 |
variant | "primary" | "secondary" | "primary" | 视觉变体。primary 为默认带阴影样式;secondary 为低强调无阴影,适用于 Surface 内 |
children | React.ReactNode | - | InputOTP.Group、InputOTP.Slot 与 InputOTP.Separator 组件 |
Validation Props
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
isDisabled | boolean | false | 是否禁用 |
isInvalid | boolean | false | 是否处于无效状态 |
validationErrors | string[] | - | 服务端或自定义校验错误 |
validationDetails | ValidityState | - | HTML5 校验详情 |
Input Props
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
pattern | string | - | 允许字符的正则模式(如 REGEXP_ONLY_DIGITS) |
textAlign | 'left' | 'center' | 'right' | 'left' | slot 内文本对齐 |
inputMode | 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url' | 'numeric' | 移动设备虚拟键盘类型 |
placeholder | string | - | 空 slot 的占位文本 |
pasteTransformer | (text: string) => string | - | 转换粘贴文本(如移除连字符) |
Form Props
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
name | string | - | 表单提交的 name 属性 |
autoFocus | boolean | - | 挂载时是否聚焦第一个 slot |
InputOTP.Group
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | 组的附加 CSS 类 |
children | React.ReactNode | - | InputOTP.Slot 组件 |
InputOTP.Slot
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
index | number | - | 必填。 slot 的从零开始索引 |
className | string | - | slot 的附加 CSS 类 |
InputOTP.Separator
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | 分隔符的附加 CSS 类 |
Exported Patterns
HeroUI 重新导出 input-otp 的常用正则模式以便使用:
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_CHARS, REGEXP_ONLY_DIGITS_AND_CHARS } from '@lenso/ui';
// Use with pattern prop<InputOTP pattern={REGEXP_ONLY_DIGITS} maxLength={6}> {/* ... */}</InputOTP>- REGEXP_ONLY_DIGITS - 仅数字字符(0-9)
- REGEXP_ONLY_CHARS - 仅字母字符(a-z、A-Z)
- REGEXP_ONLY_DIGITS_AND_CHARS - 字母数字字符(0-9、a-z、A-Z)