Ir al contenido
Formularios

OTPField

El código de verificación: una casilla por dígito y un solo valor, con el autorrelleno del SMS.

import { OTPField } from "sebs7n-ui/otp-field"

Ejemplos

Verificar un mail

`onValueComplete` avisa cuando entra el último dígito: en un código de seis cifras el botón "Continuar" sobra, porque no hay nada más que decidir.

import { Field, FieldDescription, FieldLabel } from "sebs7n-ui/field"
import { OTPField } from "sebs7n-ui/otp-field"
import { useState } from "react"

function VerificarMail() {
  const [codigo, setCodigo] = useState("")
  const verificado = codigo === "482913"
  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <Field name="codigo">
        <FieldLabel>Código de verificación</FieldLabel>
        <OTPField onValueComplete={setCodigo} />
        <FieldDescription>Te lo mandamos a hola@acme.com. Probá con 482913.</FieldDescription>
      </Field>
      {codigo.length === 6 && (
        <p className={verificado ? "text-callout text-blue-900" : "text-callout text-red-900"}>
          {verificado ? "Mail verificado." : "Ese código no es el que mandamos."}
        </p>
      )}
    </div>
  )
}

2FA al entrar

El código viaja como un solo valor bajo el `name` del campo. Si está incompleto, el formulario no sale: lo valida el input escondido que tiene el código entero, no cada casilla por separado.

import { Button } from "sebs7n-ui/button"
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Form } from "sebs7n-ui/form"
import { OTPField } from "sebs7n-ui/otp-field"
import { useState } from "react"

function DosFactores() {
  const [enviado, setEnviado] = useState("")
  return (
    <Form className="flex w-full max-w-sm flex-col gap-4" onFormSubmit={(valores) => setEnviado(String(valores.codigo))}>
      <Field name="codigo" validate={(valor) => (String(valor).length === 6 ? null : "Faltan dígitos: el código tiene seis.")}>
        <FieldLabel required>Código de tu app de autenticación</FieldLabel>
        <OTPField />
        <FieldError />
      </Field>
      <Button type="submit">Entrar</Button>
      {enviado && <p className="text-callout text-label-secondary">Se envió {enviado}.</p>}
    </Form>
  )
}

Otros largos y tamaños

`length` cambia la cantidad de casillas; `size` usa las mismas alturas que `Input`, para que un OTP en medio de un formulario no desentone.

import { OTPField } from "sebs7n-ui/otp-field"

function LargosYTamanos() {
  return (
    <div className="flex flex-col gap-4">
      <OTPField aria-label="Código de cuatro dígitos" length={4} size="sm" />
      <OTPField aria-label="Código de seis dígitos" />
      <OTPField aria-label="Código de ocho dígitos" length={8} size="lg" />
    </div>
  )
}

Props

Generadas del TypeScript del paquete. Las propias del componente, más las heredadas del primitivo que tienen algo que explicar —marcadas «heredada de Base UI»—. El resto está en la línea «hereda de».

OTPField

Hereda las props de OTPField.Root.

PropTipoPor defectoDescripción
inputClassNamestring—Clases de cada casilla, para tocar el ancho o el tipo de letra sin reescribir el componente.
lengthnumber6Cuántas casillas. Seis es lo que manda casi todo el mundo por SMS.
size"sm" | "md" | "lg""md"sm 28 px · md 36 px · lg 40 px, las mismas alturas que Input.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

Teclado

Tab
Entra y sale del campo entero. Adentro hay una sola casilla tabulable, la activa.
0-9
Escribe en la casilla y salta a la siguiente.
Backspace
Borra el dígito y retrocede. Con ⌘/Ctrl borra el código entero.
Delete
Borra el dígito sin moverse de casilla.
← →
Se mueve entre casillas. Con ⌘/Ctrl va al principio o al final.
Home / End
Primera casilla · última casilla escrita.
⌘V
Pega el código completo y lo reparte entre las casillas.

Accesibilidad

  • El contenedor es un role="group" nombrado por el FieldLabel, y cada casilla hereda ese nombre: se anuncia un campo con nombre, no seis campos de texto anónimos.
  • Debajo viaja un input oculto con el valor entero: es el que lleva el name, el required y el largo, así que la validación y el submit hablan del código, no de un dígito.
  • Solo la casilla activa queda en el orden de tabulación: se entra y se sale con un Tab, no con seis.
  • autoComplete="one-time-code" va en la primera casilla y en el input oculto: es lo que hace que iOS y Android ofrezcan el código del SMS. Pisarlo lo apaga.
  • El error lo anuncia FieldError una sola vez, sobre el grupo. aria-invalid aparece cuando la invalidez la declara la app (<Field invalid> o un error del servidor); la que calcula el navegador pinta con data-invalid.

Reglas de uso

  • Siempre dentro de un `Field` con `FieldLabel`. Sin etiqueta, seis casillas son seis cajas sin nombre.
  • Seis casillas y `validationType="numeric"` salvo que el proveedor mande otra cosa: es lo que la gente espera y lo que pone el teclado numérico en el celular.
  • onValueComplete en lugar de un botón «Continuar»: cuando entra el último dígito no queda nada que decidir. Dejá el botón solo si el envío cuesta plata o es irreversible.
  • autoSubmit manda el formulario solo al completarse. Úsalo cuando el error se puede reintentar sin costo; si no, el envío accidental es peor que un click de más.
  • mask tapa los dígitos como una contraseña: casi nunca hace falta. Ver lo que se escribió es justamente lo que evita el segundo intento.
  • No lo uses para un CUIT, una tarjeta ni un teléfono: esos son un Input con inputMode, porque se copian, se corrigen al medio y no tienen largo fijo de una cifra por casilla.

Relacionados