DEV Community

Cover image for FormValidation is discontinued — here's the TypeScript successor I built
Vinicius
Vinicius

Posted on

FormValidation is discontinued — here's the TypeScript successor I built

If you've ever worked on a complex form without a framework, there's a good chance you've used or at least heard of FormValidation. It was the go-to solution for plugin-based, framework-agnostic form validation for years.

Then it went silent. No updates, no responses to issues. Just an archived library slowly rotting against modern JavaScript.

I needed a replacement for a project. I couldn't find one that matched the flexibility of the plugin model, so I built Validare — a modern TypeScript rewrite of the same architecture, from scratch.


What Validare is

@validare/core is a form validation library with:

  • 51 built-in validators — from notEmpty and email to iban, creditCard, uuid, vin, and more
  • 17 plugins — UI feedback, accessibility, CSS framework integrations, async flows
  • TypeScript-first — full type safety, no @types/* packages needed
  • Zero dependencies — no jQuery, no frameworks, no polyfills
  • ESM + CJS + UMD — works everywhere

The core idea is the same as FormValidation: a small engine that does nothing on its own, extended entirely through plugins. You add exactly what you need.


Quick start

npm install @validare/core
Enter fullscreen mode Exit fullscreen mode
<form id="myForm" novalidate>
  <input type="email" name="email" />
  <div class="vd-plugins-message-container"></div>
  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode
import { validare, Trigger, Message, SubmitButton } from '@validare/core';

const fv = validare(document.getElementById('myForm'), {
  fields: {
    email: {
      validators: {
        notEmpty: { message: 'Email is required' },
        email:    { message: 'Please enter a valid email address' },
      },
    },
  },
  plugins: {
    trigger:      new Trigger({ event: 'blur' }),
    message:      new Message(),
    submitButton: new SubmitButton(),
  },
});

document.getElementById('myForm').addEventListener('submit', async (e) => {
  e.preventDefault();
  const result = await fv.validate(); // 'Valid' | 'Invalid' | 'NotValidated'
  if (result === 'Valid') {
    // safe to submit
  }
});
Enter fullscreen mode Exit fullscreen mode

That's it. The Trigger plugin wires up DOM events, Message renders error messages, and SubmitButton disables the button during validation.


The plugin model

Plugins are where Validare earns its keep. Each one is a focused, composable unit:

Plugin What it does
Trigger Validates on DOM events (blur, input, change)
Message Renders error messages in the DOM
Icon Shows ✓ / ✗ icons next to fields
Bootstrap5 Applies is-valid / is-invalid classes
Tailwind Applies configurable Tailwind utility classes
Aria Adds aria-invalid and aria-describedby automatically
Sequence Stops at the first failing validator per field
Excluded Skips disabled or hidden fields
Declarative Configures validators via data-vd-* HTML attributes
PasswordStrength Scores password strength (0–4)
CharCounter Live character counter with overflow state
Summary Renders a consolidated error list in a container
Transformer Normalises field values before validation (trim, strip, etc.)

You compose only what your project needs:

plugins: {
  trigger:   new Trigger({ event: 'input' }),
  message:   new Message(),
  ui:        new Bootstrap5(),
  aria:      new Aria(),
  sequence:  new Sequence(),
  excluded:  new Excluded(),
}
Enter fullscreen mode Exit fullscreen mode

51 validators, organized by category

Core (23): notEmpty, blank, email, creditCard, date, digits, integer, numeric, regexp, uri, identical, different, between, greaterThan, lessThan, stringLength, stringCase, choice, file, callback, promise, remote, ip

Format & Encoding: base64, hex, mac, bic, uuid, color

Financial: iban, vat, cusip, isin, sedol, grid

Publication: ean, isbn, ismn, issn

Device & Vehicle: imei, imo, meid, step, vin

Tax & Business: ein, rtn, siren, siret

Identity & Geographic: id, phone, zipCode


Async validation and remote checks

The remote validator hits an endpoint and expects { valid: boolean }:

username: {
  validators: {
    notEmpty: { message: 'Username is required' },
    remote: {
      url: '/api/check-username',
      method: 'POST',
      message: 'This username is already taken',
    },
  },
},
Enter fullscreen mode Exit fullscreen mode

Or write your own async validator with callback or promise:

validators: {
  callback: {
    message: 'Invalid value',
    callback: async ({ value }) => {
      const res = await fetch(`/api/validate?q=${value}`);
      const { valid } = await res.json();
      return { valid };
    },
  },
},
Enter fullscreen mode Exit fullscreen mode

Writing a custom validator

Every validator is a zero-argument factory:

import type { ValidatorFactory } from '@validare/core';

export const noSpaces: ValidatorFactory = () => ({
  validate({ value }) {
    return { valid: !value.includes(' '), message: 'No spaces allowed' };
  },
});
Enter fullscreen mode Exit fullscreen mode

Use it directly in your config — no registration needed:

import { noSpaces } from './validators/noSpaces';

fields: {
  username: {
    validators: {
      notEmpty: { message: 'Required' },
      noSpaces: noSpaces,
    },
  },
},
Enter fullscreen mode Exit fullscreen mode

Coming from FormValidation?

The architecture is intentionally familiar. The main differences:

  • Package name: @validare/core (not @form-validation/cjs)
  • CSS classes: vd-plugins-* instead of fv-plugins-*
  • HTML attributes (Declarative plugin): data-vd-* instead of data-fv-*
  • Native TypeScript — no extra type packages

A full migration guide is available in MIGRATING.md.


Try it

Feedback, issues, and PRs are very welcome. This is an active project and I'm happy to prioritize validators or plugins based on what people actually need.

Top comments (0)