Skip to content

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' } },
},
});

Each field’s rules is an object: rule name as the key, error message as the value.

required: 'Email is required'Your message.
email: nullThe 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]': nullSeveral 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.

GroupRules
Presencerequired, accepted (for checkboxes: passes for on, yes, 1, true), boolean
Formatemail, url, active_url, ip, ipv4, ipv6, uuid, alpha, alpha_dash, alpha_num
Numbersnumeric, integer, min[n], max[n], gt[field], gte[field], lt[field], lte[field]
Stringsmin_length[n], max_length[n], starts_with[a,b], doesnt_start_with[a,b], doesnt_end_with[a,b], regex[pattern], not_regex[pattern]
Setsin[a,b,c], not_in[a,b,c], same[field]
Datesdate, after[date], before[date], date_equals[date]

Three things the server does with parameters:

  • min[n] and max[n] are numeric bounds: the server adds numeric to the field. For string length use min_length[n] and max_length[n], which add string.
  • 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. Prefer alpha_num, starts_with and friends, or re-validate on your own backend.
  • same[field], gt[field] and the other comparisons name another field in the same form.

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 undefined

Every 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.

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>
))}
<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; }.

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.

const errors = await form.validate(); // all fields; applied to form.errors
const emailErrors = await form.validate('email'); // one field; only its errors applied

Returns 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);
};

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.