Quick start
npm install @sparkstone/solid-validationpnpm add @sparkstone/solid-validationSolid 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:
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
| Event | What happens |
|---|---|
| Blur | The field is checked. A failure writes to errors and sets aria-invalid="true". |
| Input | If that field is showing an error, it clears and aria-invalid goes back to "false". |
| Submit | Every field is checked in order. The first failure is focused and scrolled to, and your callback does not run. |
| Reset | All 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.