Client-side validation

There are two client-side validation options with Superforms:

  • The built-in browser validation, which doesn’t require JavaScript to be enabled in the browser.
  • Using a validation schema, usually the same one as on the server. Requires JavaScript and use:enhance.

Built-in browser validation

There is a web standard for form input, which doesn’t require JavaScript and is virtually effortless to use with Superforms:


To use the built-in browser validation, just spread the $constraints store for a schema field on its input field:

<script lang="ts">
  export let data;
  const { form, constraints } = superForm(data.form);

  {...$} />

The constraints is an object with validation properties mapped from the schema:

  pattern?: string;      // The *first* string validator with a RegExp pattern
  step?: number | 'any'; // number with a step validator
  minlength?: number;    // string with a minimum length
  maxlength?: number;    // string with a maximum length
  min?: number | string; // number if number validator, ISO date string if date validator
  max?: number | string; // number if number validator, ISO date string if date validator
  required?: true;       // true if not nullable, nullish or optional

Special input formats

For some input types, a certain format is required. For example with date fields, both the underlying data and the constraint needs to be in yyyy-mm-dd format, which can be handled by using a proxy and adding attributes after the constraints spread, in which case they will take precedence:

  aria-invalid={$ ? 'true' : undefined}
  min={$, 10)} 

Check the validation attributes and their valid values at MDN.

Using a validation schema

The built-in browser validation can be a bit constrained; for example, you can’t easily control the position and appearance of the error messages. Instead (or supplementary), you can use a validation schema and customize the validation with a number of options, so the form errors will be displayed in real-time.

const { form, enhance, constraints, validate, validateForm } = superForm(data.form, {
  validators: ClientValidationAdapter<S> | 'clear' | false,
  validationMethod: 'auto' | 'oninput' | 'onblur' | 'submit-only' = 'auto',
  customValidity: boolean = false


validators: ClientValidationAdapter<S> | 'clear' | false

Setting the validators option to an adapter with the same schema as on the server, is the most convenient and recommended way. Just put the schema in a shared directory, $lib/schemas for example, and import it on the client as well as on the server.

Adding a adapter on the client will increase the client bundle size a bit, since the validation library now has to be imported there too. But the client-side adapter is optimized to be as small as possible, so it shouldn’t be too much of an issue. To use it, append Client to the adapter import, for example:

import { valibotClient } from 'sveltekit-superforms/adapters';
import { schema } from './schema.js';

const { form, errors, enhance } = superForm(data.form, {
  validators: valibotClient(schema)

As a super-simple alternative to an adapter, you can also set the option to 'clear', to remove errors for a field as soon as it’s modified.

Switching schemas

You can even switch schemas dynamically, with the options object. options.validators accepts a partial validator, which can be very useful for multi-step forms:

import { valibot } from 'sveltekit-superforms/adapters';
import { schema, partialSchema } from './schema.js';

const { form, errors, enhance, options } = superForm(data.form, {
  // Validate the first step of the form
  validators: valibot(partialSchema)

// When moving to the last step of the form
options.validators = valibot(schema)

As mentioned, you need the full adapter to switch schemas dynamically. An exception will be thrown if a client adapter is detected different from the initial validators option.


validationMethod: 'auto' | 'oninput' | 'onblur' | 'onsubmit',

The validation is triggered when a value is changed, not just when focusing on, or tabbing through a field. The default validation method is based on the “reward early, validate late” pattern, a researched way of validating input data that makes for a high user satisfaction:

  • If entering data in a field that has or previously had errors, validate on input
  • Otherwise, validate on blur.

But you can instead use the oninput or onblur settings to always validate on one of these respective events, or onsubmit to validate only on submit.


This option uses the browser built-in tooltip to display validation errors, so neither $errors nor $constraints are required on the form. See the error handling page for details and an example.


The validate function, deconstructed from superForm, gives you complete control over the validation process for specific fields. Examples of how to use it:

const { form, enhance, validate } = superForm(data.form, {
  validators: zod(schema) // Required option for validate to work

// Simplest case, validate what's in the field right now

// Validate without updating, for error checking
const nameErrors = await validate('name', { update: false });

// Validate and update field with a custom value
validate('name', { value: 'Test' });

// Validate a custom value, update errors only
validate('name', { value: 'Test', update: 'errors' });

// Validate and update nested data, and also taint the field
validate('tags[1].name', { value: 'Test', taint: true });


Similar to validate, validateForm lets you validate the whole form and return a SuperValidated result:

const { form, enhance, validateForm } = superForm(data.form, {
  validators: zod(schema) // Required option for validate to work

const result = await validateForm();

if (result.valid) {
  // ...

// You can use the update option to trigger a client-side validation
await validateForm({ update: true });

// Or the schema option to validate the form partially
const result2 = await validateForm({ schema: zod(partialSchema) });

Asynchronous validation and debouncing

The validation is asynchronous, but a slow validator will delay the final result, so for a server round-trip validation, like checking if a username is available, you might want to consider a SPA action form, or handle it with on:input and a package like throttle-debounce. Errors can be set manually by updating the $errors store:

// Needs to be a string[]
$errors.username = ['Username is already taken.'];

Test it out

This example demonstrates how validators are used to check if tags are of the correct length.

Set a tag name to blank and see that no errors show up until you move focus outside the field (blur). When you go back and correct the mistake, the error is removed as soon as you enter more than one character (input).

Found a typo or an inconsistency? Make a quick correction here!