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>
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)
})
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'
}})
}
})
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.
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)
Single-boolean checkbox, styled switch, fieldset/legend radio group — all reactive. Watch the live snapshot update.
form.watch(function (values) {
render(values) // any change
})
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'}]
setValues, setError, reset,
disable — the instance is the single source of truth.
form.setValues({ city: 'Berlin' })
form.setError('city', 'Not deliverable')
form.isDirty() // true