Vanilla UI Kit Form v1.0.0 · toast → select →

Forms, one file.

Zero-dependency reactive forms: schema or enhancement, sync + async validation, loading-state submission, server error mapping, and a honeypot bots fall into. Every card below is live.

<script src="https://cdn.jsdelivr.net/gh/vanilla-ui-kit/components/form/form.js"></script>

Schema mode — signup

Async username check (try taken), password toggle, fake 2s submit with loading state and success.

new Form('#signup', {
  fields: [
    { name: 'username', required: true,
      validate: v => checkTaken(v) },  // async!
    { name: 'email', type: 'email', required: true },
    { name: 'password', type: 'password',
      required: true, minlength: 8 }
  ],
  onSubmit: values => api.signup(values)
})

Enhance mode — contact

A plain HTML <form>, adopted: its required/type attributes seed the validators. Submitting simulates a server 422 whose {errors:{…}} body is mapped onto the fields.

new Form(formEl, {
  onSubmit: function () {
    // server says 422 {errors:{email:…}}
    return Promise.reject({ errors: {
      email: 'This address is blocked',
      message: 'Please keep it under 500 chars'
    }})
  }
})

Honeypot — what bots see

An off-viewport decoy input (not display:none — bots check for that) plus a time gate. Fill it "like a bot" and the form pretends to succeed while onSpam logs the catch. Humans never see it: it sits at left:-9999px, out of the tab order, hidden from screen readers.

spam log: (empty)
new Form('#el', {
  honeypot: true,        // default
  minFillTime: 1500,     // ms time gate
  onSpam: values => log(values)
})
// client honeypots stop dumb bots only —
// ALWAYS validate on the server too (_hp_t helps)

Checks, switches, radios

Single-boolean checkbox, styled switch, fieldset/legend radio group — all reactive. Watch the live snapshot update.

form.watch(function (values) {
  render(values)   // any change
})

Family upgrades

This page loads ../select/select.js and ../datepicker/datepicker.js, so the select and date fields below auto-upgrade to the family widgets. The phone field stays a native <input type=tel> because no PhoneInput is loaded — graceful degradation, no code changes.

<script src="../select/select.js"></script>
<script src="../datepicker/datepicker.js"></script>
<script src="./form.js"></script>
// fields: [{name:'plan', type:'select', …},
//          {name:'start', type:'date'},
//          {name:'phone', type:'phone'}]

Programmatic API

setValues, setError, reset, disable — the instance is the single source of truth.

form.setValues({ city: 'Berlin' })
form.setError('city', 'Not deliverable')
form.isDirty()  // true