Validation
Validation runs on the server, with Laravel’s validator, against the rules you declare in fields. The package never validates locally, so what the visitor sees is exactly what the server decided.
const form = useSnipForm({ key, fields: { email: { type: 'email', rules: { required: 'Email is required', email: 'That does not look like an email' } }, age: { type: 'number', rules: { 'min[18]': 'You must be 18 or older' } }, password: { type: 'password', rules: { required: null, 'min_length[8]': null } }, confirm: { type: 'password', rules: { 'same[password]': 'Passwords do not match' } }, },});The rules map
Section titled “The rules map”Each field’s rules is an object: rule name as the key, error message as the value.
required: 'Email is required' | Your message. |
email: null | The server’s default message, for example “The email field must be a valid email address.” |
'min[18]': '...' | A rule with a parameter. Square brackets, exactly as the HTML library’s sf-validate:min[18]. |
'in[starter,pro,team]': null | Several parameters, comma separated. |
Rules are checked in the order you list them and every failing rule contributes a message. A field with no rules (or rules: {}) is optional and accepts any value.
The same set as the HTML library, where each rule has a longer description.
| Group | Rules |
|---|---|
| Presence | required, accepted (for checkboxes: passes for on, yes, 1, true), boolean |
| Format | email, url, active_url, ip, ipv4, ipv6, uuid, alpha, alpha_dash, alpha_num |
| Numbers | numeric, integer, min[n], max[n], gt[field], gte[field], lt[field], lte[field] |
| Strings | min_length[n], max_length[n], starts_with[a,b], doesnt_start_with[a,b], doesnt_end_with[a,b], regex[pattern], not_regex[pattern] |
| Sets | in[a,b,c], not_in[a,b,c], same[field] |
| Dates | date, after[date], before[date], date_equals[date] |
Three things the server does with parameters:
min[n]andmax[n]are numeric bounds: the server addsnumericto the field. For string length usemin_length[n]andmax_length[n], which addstring.regex[pattern]is wrapped in/delimiters by the server, so write the bare pattern:'regex[^SF-\\d+$]'. Square brackets inside a parameter are not supported (the parser splits on them), which rules out character classes. Preferalpha_num,starts_withand friends, or re-validate on your own backend.same[field],gt[field]and the other comparisons name another field in the same form.
Errors on the handle
Section titled “Errors on the handle”After a submit that fails validation the hook sets status back to ready and fills errors:
form.errors// { email: ['Email is required'], confirm: ['Passwords do not match'] }
form.fieldError('email') // 'Email is required' (the first message) or undefinedEvery message is kept, not just the first. register() sets aria-invalid={true} on inputs that have errors, and the onValidationError(errors) callback fires with the same bag.
<FieldError>
Section titled “<FieldError>”import { FieldError } from '@snipform/react';
<FieldError form={form} name="email" /><FieldError form={form} name="email" as="span" className="text-sm text-red-600" />Renders the first message inside a <p> (or the element named by as) with role="alert", or renders nothing. To show every message, read form.errors[name] yourself:
{form.errors.password?.map((message) => ( <p key={message} role="alert">{message}</p>))}Styling an invalid input
Section titled “Styling an invalid input”<input {...form.register('email')} className={form.errors.email ? 'border-red-500' : 'border-gray-300'}/>Or target the attribute in CSS: input[aria-invalid="true"] { border-color: red; }.
Validate on blur
Section titled “Validate on blur”const form = useSnipForm({ key, fields, validateOn: 'blur' });With validateOn: 'blur', leaving a field sends the current values to the server’s validate-only endpoint, POST /v2/form/{token}/validate, which runs the real rules against the open session without consuming it or saving anything. The call is debounced 250ms, and only the blurred field’s messages are applied, so fields the visitor has not reached yet stay clean. Submit still validates everything.
What this is not: it is not local validation. Each blur is a request, and a transport failure during a blur check is swallowed rather than shown. The visitor always gets the authoritative answer on submit.
Because blur validation needs an open session, the first blur after mount opens one if the visitor’s interaction has not already done so.
validate() on demand
Section titled “validate() on demand”const errors = await form.validate(); // all fields; applied to form.errorsconst emailErrors = await form.validate('email'); // one field; only its errors appliedReturns the error bag ({} when clean, or when no session is open yet). Useful for multi-step forms: validate the current step’s fields before moving on.
const next = async () => { const errors = await form.validate(); const stepClean = ['name', 'email'].every((field) => !errors[field]); if (stepClean) setStep(2);};Messages and field names
Section titled “Messages and field names”Default messages use the field name humanised: a field called first_name reads as “The first name field is required.” Name fields the way you would want them read, or supply your own message for every rule the visitor can realistically hit.