Skip to content

Quick start

Terminal window
npm install @sparkstone/solid-validation
Terminal window
pnpm add @sparkstone/solid-validation

Solid 1.9 or newer is the only peer dependency.

A complete form

Everything below runs as written. Copy the whole file.

import { useForm, type Validator } from '@sparkstone/solid-validation';
const minLength =
(min: number): Validator<HTMLInputElement> =>
el =>
el.value.length < min && `Use at least ${min} characters`;
export default function SignupForm() {
const { formSubmit, validate, errors, isSubmitting, isSubmitted } = useForm();
async function onSubmit(form: HTMLFormElement) {
await fetch('/api/signup', { method: 'POST', body: new FormData(form) });
}
return (
<form use:formSubmit={onSubmit}>
<label for='email'>Email</label>
<input id='email' type='email' name='email' required use:validate />
<span>{errors.email}</span>
<label for='handle'>Handle</label>
<input id='handle' name='handle' required use:validate={[minLength(3)]} />
<span>{errors.handle}</span>
<button type='submit' disabled={isSubmitting()}>
{isSubmitting() ? 'Saving' : 'Save'}
</button>
{isSubmitted() && <p>Saved</p>}
</form>
);
}

That is the whole API for a normal form. Here it is running:

Tab out of a field without filling it in. Validation runs on blur, and the error clears the moment you start typing again.

What each piece does

useForm() returns everything the form needs. Destructuring it is required, not stylistic: use:validate only compiles when validate exists as a variable in scope.

use:validate puts a field in the form. With no value it checks the browser’s own rules, which is where required and type="email" are handled. With an array it runs your validators after those pass.

use:formSubmit takes over the submit event. Your callback runs only once every field passes, and receives the form element.

errors is a store keyed by each field’s name. Render errors.email wherever the message should appear.

When validation happens

EventWhat happens
BlurThe field is checked. A failure writes to errors and sets aria-invalid="true".
InputIf that field is showing an error, it clears and aria-invalid goes back to "false".
SubmitEvery field is checked in order. The first failure is focused and scrolled to, and your callback does not run.
ResetAll errors clear.

Nothing is checked while you are still filling in an untouched field, which is what keeps the form quiet.

Styling invalid fields

Failing fields get aria-invalid="true":

input[aria-invalid='true'] {
border-color: red;
}

Prefer a class? Pass one and it is added and removed alongside the attribute:

const { validate } = useForm({ errorClass: 'is-invalid' });

Next

Server errors, async checks, and fields outside a form are all in the guides. If use:validate reports a type error or seems to do nothing, see Troubleshooting.