react-formbridge
Browse documentation
Field buildersv1.0.2

field.masked()

String input constrained by a mask pattern or preset. Ideal for credit cards, expiry dates, ZIP codes, and formatted identifiers.

  • First argument is required: a built-in MASKS preset, a raw pattern string, or a { pattern, tokens } object
  • Label the field separately with label(...) since the constructor takes the mask, not the label
  • The renderer adapts width and input mode to the mask profile (numeric vs alphanumeric)
import { useState } from 'react'
import { MASKS, field, useFormBridge } from '@runilib/react-formbridge'

const schema = {
  cardNumber: field
    .masked(MASKS.CARD_16)
    .label('Card number')
    .required()
    .showMaskInPlaceholder()
    .validateComplete('Card is incomplete.'),

  licensePlate: field
    .masked('LL-999-LL')
    .label('License plate')
    .tokens({
      L: /[A-Z]/,
    })
    .uppercase(),
}

export function MaskedPlaygroundWeb() {
  const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null)
  const { Form, fields, state } = useFormBridge(schema, {
    validateOn: 'onBlur',
  })

  return (
    <div
      style={{
        fontFamily: 'sans-serif',
        padding: 20,
        background: '#f5f7fb',
        display: 'grid',
        gap: 16,
      }}
    >
      <div>
        <h3 style={{ margin: '0 0 8px' }}>Interactive masked fields</h3>
        <p style={{ margin: 0, color: '#4b5563' }}>
          Try the card mask and the custom plate mask, then inspect the stored values.
        </p>
      </div>

      <Form
        onSubmit={async (values) => {
          setSubmitted(values)
        }}
      >
        <div style={{ display: 'grid', gap: 12 }}>
          <fields.cardNumber />
          <fields.licensePlate />
          <Form.Submit>Save values</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 MaskedPlaygroundWeb

Defaults, inheritance & field methods

  • First argument is required: a MASKS preset, a custom pattern string, or a { pattern, tokens } object
  • defaultValue is ''
  • Masked values are stored by default (separators like /, -, spaces are preserved)
  • Shared methods: see Base field builder
  • String builder methods: see field.text()

Mask-specific methods:

MethodTypeDescription
storeRaw()() => thisStores the unformatted raw payload instead of the masked value.
storeMasked()() => thisExplicitly keeps the formatted masked value, which is the default behavior.
showPlaceholder(char?)char?: stringRenders placeholder characters inside the current value.
showMaskInPlaceholder(charOrText?)charOrText?: stringRenders the mask as placeholder text while keeping the actual value empty.
tokens(map)map: Record<string, RegExp>Adds or overrides token characters for advanced masks.
validateComplete(message?)message?: stringRequires the entire mask to be filled before submit.

Adaptive mask runtime

The built-in masked renderers now adapt to the mask instead of treating every mask like the same numeric field.

  • Numeric-only masks keep numeric-friendly keyboard / input mode hints
  • Alphanumeric masks stop behaving like numeric-only inputs when the token map accepts letters
  • maxLength follows the visible mask length, including separators
  • Short masks stay compact while longer masks can claim the width they need, which makes side-by-side layouts easier without hand-tuned widths

If you want the mask behavior but your own shell, prefix badge, trigger row, or fully custom layout, keep the field as field.masked(...) in the schema and drive the UI through form.fieldController(name).

Recipes

Patterns that showcase mask-specific strengths.

Credit card with placeholder

CardNumber.tsxtsx
1import { MASKS } from '@runilib/react-formbridge'
2
3const schema = {
4 cardNumber: field.masked(MASKS.CARD_16)
5 .label('Card number')
6 .required()
7 .showMaskInPlaceholder()
8 .validateComplete('Card number is incomplete.'),
9}

Expiry + CVV side by side

ExpiryCvv.tsxtsx
1import { MASKS } from '@runilib/react-formbridge'
2
3const schema = {
4 expiry: field.masked(MASKS.EXPIRY).label('Expiry').required(),
5 cvv: field.masked(MASKS.CVV).label('CVV').required(),
6}

Raw backup code (strip separators)

BackupCode.tsxtsx
1const schema = {
2 backupCode: field.masked('9999-9999')
3 .label('Backup code')
4 .storeRaw()
5 .validateComplete(),
6}

Custom plate with uppercase token

LicensePlate.tsxtsx
1const schema = {
2 plate: field.masked('LL-999-LL')
3 .label('Plate')
4 .tokens({ L: /[A-Z]/ })
5 .uppercase(),
6}

French IBAN

IbanFr.tsxtsx
1import { MASKS } from '@runilib/react-formbridge'
2
3const schema = {
4 iban: field.masked(MASKS.IBAN_FR)
5 .label('IBAN')
6 .required()
7 .validateComplete('IBAN is incomplete.'),
8}

Pattern syntax

  • 9 = digit
  • a = letter
  • * = any character
  • Any other character is treated as a visible separator
  • Use tokens({...}) to add custom mask characters such as L for uppercase-only letters

Built-in MASKS presets

Pass any of these presets as field.masked(MASKS.X).label('Label').

Cards

CARD_16
9999 9999 9999 9999
CARD_AMEX
9999 999999 99999
CARD_19
9999 9999 9999 9999 999

Security / expiry

CVV
999
CVV_AMEX
9999
EXPIRY
99/99

Date / time

DATE_DMY
99/99/9999
DATE_MDY
99/99/9999
DATE_ISO
9999-99-99
TIME_HM
99:99
TIME_HMS
99:99:99
DATETIME
99/99/9999 99:99

Bank / finance

IBAN_FR
aa99 9999 9999 9999 9999 9999 999
IBAN_DE
aa99 9999 9999 9999 9999 99
IBAN_GB
aa99 aaaa 9999 9999 9999 99
IBAN
aa99 9999 9999 9999 9999 9999 9999 99
BANK_NZ
99-9999-9999999-99
SIREN
999 999 999
SIRET
999 999 999 99999

Postal / identity

ZIP_FR
99999
ZIP_US
99999
ZIP_US_PLUS4
99999-9999
POSTCODE_UK
aa9 9aa
SSN
999-99-9999
NIR_FR
9 99 99 99 999 999 99

Other

IP_ADDRESS
999.999.999.999
DURATION
99:99:99.999
NUMBER_FR
9 999 999