react-formbridge
Browse documentation
Field buildersv1.0.2

field.otp()

One-time-password builder for short verification codes. Renders individual character cells instead of a single input.

  • length() fixes the exact code length and the renderer shows that many cells
  • digitsOnly() restricts to numeric input and hints a numeric keyboard
  • mask() hides the typed value behind a display character (e.g. ) while keeping the real value in form state
  • groups() splits the code into groups with a non-editable separator between them, like ___-__
  • Combine with validateOn: 'onChange' at hook level for instant validation as the user types
import { useState } from 'react'
import { field, useFormBridge } from '@runilib/react-formbridge'

const schema = {
  code: field.otp('Verification code')
    .groups([3, 3], '-')
    .digitsOnly()
    .required(),
  plainCode: field.otp('Verification code without groups')
    .length(6)
    .digitsOnly()
    .required(),
}

export function OtpPlaygroundWeb() {
  const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null)
  const { Form, fields, state } = useFormBridge(schema, {
    validateOn: 'onChange',
  })
  const code = String(state.values.code ?? '')
  const plainCode = String(state.values.plainCode ?? '')

  return (
    <div
      style={{
        fontFamily: 'sans-serif',
        padding: 20,
        background: '#f5f7fb',
        display: 'grid',
        gap: 16,
      }}
    >
      <div>
        <h3 style={{ margin: '0 0 8px' }}>Interactive OTP field</h3>
        <p style={{ margin: 0, color: '#4b5563' }}>
          Type a 6-digit code to see grouped cells, live validation, and the submitted payload.
        </p>
      </div>

      <Form
        onSubmit={async (values) => {
          setSubmitted(values)
        }}
      >
        <div style={{ display: 'grid', gap: 12 }}>
          <fields.code />
          <p style={{ margin: 0, color: '#4b5563' }}>
            Grouped progress: <strong>{code.length}</strong> / 6
          </p>
          <fields.plainCode />
          <p style={{ margin: 0, color: '#4b5563' }}>
            Plain progress: <strong>{plainCode.length}</strong> / 6
          </p>
          <Form.Submit>Verify code</Form.Submit>
        </div>
      </Form>

      <div style={{ display: 'grid', gap: 12 }}>
        <div
          style={{
            border: '1px solid #d6d9e0',
            borderRadius: 12,
            padding: 12,
            background: '#fff',
          }}
        >
          <strong>Live values</strong>
          <pre style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
            {JSON.stringify(state.values, null, 2)}
          </pre>
        </div>

        <div
          style={{
            border: '1px solid #d6d9e0',
            borderRadius: 12,
            padding: 12,
            background: '#fff',
          }}
        >
          <strong>Last submit</strong>
          <pre style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
            {JSON.stringify(submitted, null, 2)}
          </pre>
        </div>
      </div>
    </div>
  )
}

export default OtpPlaygroundWeb

Defaults, inheritance & field methods

OTP-specific methods:

MethodTypeDescription
length(length, message?)length: numberFixes the exact expected code length and syncs the descriptor min/max values.
digitsOnly(message?)message?: stringRejects non-digit characters and hints a numeric keyboard.
mask(char?)char?: string (default '•')Renders each filled cell with a masking character while the real value stays in form state.
groups(sizes, separator?)sizes: number[], separator?: string (default '-')Splits the code into groups with a non-editable separator between them (e.g. [3, 2] renders ___-__). The total length becomes the sum of the sizes.

Recipes

Patterns that showcase otp-specific strengths.

Standard 6-digit verification

Verification6.tsxtsx
1const schema = {
2 code: field.otp('Verification code')
3 .length(6)
4 .digitsOnly()
5 .required(),
6}

Short 4-digit PIN

Pin4.tsxtsx
1const schema = {
2 pin: field.otp('PIN')
3 .length(4)
4 .digitsOnly('Digits only')
5 .required(),
6}

Alphanumeric backup code

BackupCode.tsxtsx
1const schema = {
2 backupCode: field.otp('Backup code').length(8).required(),
3}

Masked verification code

MaskedOtp.tsxtsx
1const schema = {
2 code: field.otp('Verification code')
3 .length(6)
4 .digitsOnly()
5 .mask()
6 .required(),
7}

Grouped layout with a separator

GroupedOtp.tsxtsx
1const schema = {
2 code: field.otp('Verification code')
3 .groups([3, 2], '-')
4 .digitsOnly()
5 .required(),
6}

Auto-submit when the code is full

AutoSubmitOtp.tsxtsx
1const schema = {
2 code: field.otp('Verification code').length(6).digitsOnly().required(),
3}
4
5const { Form, fields, state, submit } = useFormBridge(schema, {
6 validateOn: 'onChange',
7})
8
9useEffect(() => {
10 if (state.values.code?.length === 6 && state.isValid) {
11 submit()
12 }
13}, [state.values.code, state.isValid, submit])
14
15<Form onSubmit={verifyCode}>
16 <fields.code />
17</Form>