Validate a field inline with onValidate

Attach an onValidate handler to an input component to show a custom error message below the field when the value does not pass your rule.

Built-in validators (required, minLength, pattern, etc.) cover most constraints, but sometimes a rule is hard to express declaratively — for example "must not start with a number", "must be all uppercase", or "must be a valid IBAN". onValidate lets you write that check as a function. Returning a non-empty string displays it as an error; returning null clears any existing error.

<App>
  <Form
    data="{{ username: '', promoCode: '' }}"
    onSubmit="(data) => toast.success('Submitted: ' + JSON.stringify(data))"
    saveLabel="Submit"
  >
    <TextBox
      label="Username"
      bindTo="username"
      required="true"
      onValidate="(value) => {
        if (!value) return null;
        if (/[^a-z0-9_]/.test(value)) 
          return 'Only lowercase letters, digits, and underscores are allowed.';
        if (/^[0-9]/.test(value)) 
          return 'Username must not start with a digit.';
        return null;
      }"
      placeholder="e.g. alice_99"
    />
    <TextBox
      label="Promo code (optional)"
      bindTo="promoCode"
      onValidate="(value) => {
        if (!value) return null;
        return value === value.toUpperCase() 
          ? null : 'Promo codes must be entered in uppercase.';
      }"
      placeholder="e.g. SUMMER25"
    />
  </Form>
</App>
Inline custom field validation
<App>
  <Form
    data="{{ username: '', promoCode: '' }}"
    onSubmit="(data) => toast.success('Submitted: ' + JSON.stringify(data))"
    saveLabel="Submit"
  >
    <TextBox
      label="Username"
      bindTo="username"
      required="true"
      onValidate="(value) => {
        if (!value) return null;
        if (/[^a-z0-9_]/.test(value)) 
          return 'Only lowercase letters, digits, and underscores are allowed.';
        if (/^[0-9]/.test(value)) 
          return 'Username must not start with a digit.';
        return null;
      }"
      placeholder="e.g. alice_99"
    />
    <TextBox
      label="Promo code (optional)"
      bindTo="promoCode"
      onValidate="(value) => {
        if (!value) return null;
        return value === value.toUpperCase() 
          ? null : 'Promo codes must be entered in uppercase.';
      }"
      placeholder="e.g. SUMMER25"
    />
  </Form>
</App>

Key points

Return a string to show an error, null to clear it: The return value of onValidate is the error message. Return a non-empty string to block submission and display the text below the field. Return null, undefined, or nothing to indicate the value is valid:

<TextBox
  bindTo="username"
  onValidate="(value) => {
    if (/^[0-9]/.test(value)) return 'Cannot start with a digit.';
    return null;
  }"
/>

Guard against empty values: When the field is optional the value may be empty. Check for emptiness first and return null early so your rule does not run against a blank string — built-in required handles the empty case separately:

<TextBox
  bindTo="promoCode"
  onValidate="(value) => {
    if (!value) return null;          // let 'required' handle this if needed
    return value === value.toUpperCase() ? null : 'Must be uppercase.';
  }"
/>

onValidate runs alongside built-in validators: Built-in validators (required, minLength, maxLength, pattern, regex) always run in addition to onValidate. If a built-in validator fails first the form shows that error; once that passes, onValidate is checked:

<TextBox
  bindTo="code"
  required="true"
  minLength="6"
  onValidate="(v) => v?.startsWith('PRO') ? null : 'Code must begin with PRO.'"
/>

Validation timing is controlled by validationMode: By default errors appear only after the field loses focus or the user tries to submit (errorLate mode). Set validationMode="onChanged" to show the error live as the user types:

<TextBox
  bindTo="username"
  validationMode="onChanged"
  onValidate="(v) => /[^a-z0-9_]/.test(v) ? 'Invalid character.' : null"
/>

customValidationsDebounce delays onValidate without affecting built-ins: When onValidate makes an API call, add customValidationsDebounce (milliseconds) so it fires only after the user pauses typing. Built-in validators are unaffected and still run immediately:

<TextBox
  bindTo="username"
  required="true"
  minLength="3"
  customValidationsDebounce="400"
  onValidate="async (v) => {
    const { taken } = await checkUsername(v);
    return taken ? 'Already in use.' : null;
  }"
/>

onValidate can be async: Return a Promise<string | null>. The form waits for the promise before deciding whether to submit:

<TextBox
  bindTo="email"
  pattern="email"
  customValidationsDebounce="500"
  onValidate="async (v) => {
    if (!v) return null;
    const exists = await api.emailExists(v);
    return exists ? 'Already registered.' : null;
  }"
/>

See also