Skip to content
Lenso UI

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&apos;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&apos;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&apos;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&apos;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类型默认值描述
maxLengthnumber-必填。 输入 slot 数量
valuestring-受控值(未提供则为非受控)
onChange(value: string) => void-值变化时的回调
onComplete(value: string) => void-所有 slot 填满时的回调
classNamestring-容器的附加 CSS 类
containerClassNamestring-内部容器的 CSS 类
variant"primary" | "secondary""primary"视觉变体。primary 为默认带阴影样式;secondary 为低强调无阴影,适用于 Surface 内
childrenReact.ReactNode-InputOTP.Group、InputOTP.Slot 与 InputOTP.Separator 组件

Validation Props

Prop类型默认值描述
isDisabledbooleanfalse是否禁用
isInvalidbooleanfalse是否处于无效状态
validationErrorsstring[]-服务端或自定义校验错误
validationDetailsValidityState-HTML5 校验详情

Input Props

Prop类型默认值描述
patternstring-允许字符的正则模式(如 REGEXP_ONLY_DIGITS)
textAlign'left' | 'center' | 'right''left'slot 内文本对齐
inputMode'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url''numeric'移动设备虚拟键盘类型
placeholderstring-空 slot 的占位文本
pasteTransformer(text: string) => string-转换粘贴文本(如移除连字符)

Form Props

Prop类型默认值描述
namestring-表单提交的 name 属性
autoFocusboolean-挂载时是否聚焦第一个 slot

InputOTP.Group

Prop类型默认值描述
classNamestring-组的附加 CSS 类
childrenReact.ReactNode-InputOTP.Slot 组件

InputOTP.Slot

Prop类型默认值描述
indexnumber-必填。 slot 的从零开始索引
classNamestring-slot 的附加 CSS 类

InputOTP.Separator

Prop类型默认值描述
classNamestring-分隔符的附加 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)

相关组件