Get started

I'm using and my validation library is
npm i -D sveltekit-superforms valibot

Select your environment above and run the install command in your project folder.

If you’re starting from scratch, create a new SvelteKit project:

npm create svelte@latest

Alternatively, open the Stackblitz project to follow along in the browser and copy the code.

Creating a Superform

This tutorial will create a Superform containing a name and an email address, ready to be expanded with more form data.

Creating a validation schema

The main thing required to create a Superform is a validation schema, representing the form data for a single form.

import { object, string, email, optional } from 'valibot';

const schema = object({
  name: optional(string(), 'Hello world!'),
  email: string([email()]),
});

Schema caching

The schema should be defined outside the load function, in this case on the top level of the module. This is very important to make caching work. The adapter is memoized (cached) with its arguments, so they must be kept in memory. Therefore, define the schema, its options and eventual defaults on the top level of a module, so they always refer to the same object.

Initializing the form in the load function

To initialize the form, the schema should be used in a load function with the superValidate function:

src/routes/+page.server.ts

import { superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import { object, string, email, optional } from 'valibot';

// Define outside the load function so the adapter can be cached
const schema = object({
  name: optional(string(), 'Hello world!'),
  email: string([email()]),
});

export const load = (async () => {
  const form = await superValidate(valibot(schema));

  // Always return { form } in load functions
  return { form };
});

The Superform server API is called superValidate. You can call it in two ways in the load function:

Empty form

If you want the form to be initially empty, just pass the adapter as in the example above, and the form will be filled with default values based on the schema. For example, a string field results in an empty string, unless you have set a default.

Populate form from database

If you want to populate the form, you can send data to superValidate as the first parameter, adapter second, like this:

export const load = async ({ params }) => {
  const user = db.users.findUnique({
    where: { id: params.id }
  });

  if (!user) error(404, 'Not found');

  const form = await superValidate(user, zod(schema));

  // Always return { form } in load functions
  return { form };
};

As long as the data partially matches the schema, you can pass it directly to superValidate. This is useful for backend interfaces, where the form should usually be populated based on a url like /users/123.

Important note about return values

Unless you call the SvelteKit redirect or error functions, you should always return the form object to the client, either directly or through a helper function. The name of the variable doesn’t matter; you can call it { loginForm } or anything else, but it needs to be returned like this in all code paths that returns, both in load functions and form actions.

Displaying the form

The superValidate function returns the data required to instantiate a form on the client, now available in +page.svelte as data.form, as we did return { form }. There, we’ll use the client part of the API:

src/routes/+page.svelte

<script lang="ts">
  import { superForm } from 'sveltekit-superforms';

  export let data;

  // Client API:
  const { form } = superForm(data.form);
</script>

<form method="POST">
  <label for="name">Name</label>
  <input type="text" name="name" bind:value={$form.name} />

  <label for="email">E-mail</label>
  <input type="email" name="email" bind:value={$form.email} />

  <div><button>Submit</button></div>
</form>

The superForm function is used to create a form on the client, and bind:value is used to create a two-way binding between the form data and the input fields.

This is what the form should look like now:

Debugging

We can see that the form has been populated with the default values. But let’s add the debugging component SuperDebug to gain more insight:

src/routes/+page.svelte

<script lang="ts">
  import SuperDebug from 'sveltekit-superforms';
</script>

<SuperDebug data={$form} />

This should be displayed:

200
undefined

When editing the form fields (try in the form above), the data is automatically updated. The component also displays the current page status in the right corner.

Posting data

In the form actions, defined in +page.server.ts, we’ll use the superValidate function again, but now it should handle FormData. This can be done in several ways:

  • Use the request parameter (which contains FormData)
  • Use the event object (which contains the request)
  • Use FormData directly, if you need to access it before calling superValidate.

The most common is to use request:

src/routes/+page.server.ts

import { fail } from '@sveltejs/kit';

export const actions = {
  default: async ({ request }) => {
    const form = await superValidate(request, zod(schema));
    console.log(form);

    if (!form.valid) {
      // Again, return { form } and things will just work.
      return fail(400, { form });
    }

    // TODO: Do something with the validated form.data

    // Yep, return { form } here too
    return { form };
  }
};

Now we can post the form back to the server. Submit the form, and see what’s happening on the server:

{
  id: 'a3g9kke',
  valid: false,
  posted: true,
  data: { name: 'Hello world!', email: '' },
  errors: { email: [ 'Invalid email' ] },
}

This is the validation object returned from superValidate, containing the data needed to update the form:

Property Purpose
id Id for the schema, to handle multiple forms on the same page.
valid Tells you whether the validation succeeded or not. Used on the server and in events.
posted Tells you if the data was posted (in a form action) or not (in a load function).
data The posted data, which should be returned to the client using fail if not valid.
errors An object with all validation errors, in a structure reflecting the data.
message (optional) Can be set as a status message.

There are some other properties as well, that are only being sent in the load function:

Property Purpose
constraints An object with HTML validation constraints, that can be spread on input fields.
shape Used internally in error handling.

You can modify any of these, and they will be updated on the client when you return { form }. There are a couple of helper functions for making this more convenient, like message and setError.

Displaying errors

Now we know that validation has failed and there are errors being sent to the client. We display these by adding properties to the destructuring assignment of superForm:

src/routes/+page.svelte

<script lang="ts">
  const { form, errors, constraints } = superForm(data.form);
  //            ^^^^^^  ^^^^^^^^^^^
</script>

<form method="POST">
  <label for="name">Name</label>
  <input
    type="text"
    name="name"
    aria-invalid={$errors.name ? 'true' : undefined}
    bind:value={$form.name}
    {...$constraints.name} 
  />
  {#if $errors.name}<span class="invalid">{$errors.name}</span>{/if}

  <label for="email">E-mail</label>
  <input
    type="email"
    name="email"
    aria-invalid={$errors.email ? 'true' : undefined}
    bind:value={$form.email}
    {...$constraints.email} 
  />
  {#if $errors.email}<span class="invalid">{$errors.email}</span>{/if}

  <div><button>Submit</button></div>
</form>

<style>
  .invalid {
    color: red;
  }
</style>

As you see, by including errors, we can display errors where it’s appropriate, and through constraints (already provided by the load function), we get browser validation even without JavaScript enabled. The aria-invalid attribute is used to automatically focus on the first error field.

We now have a fully working form, with convenient handling of data and validation both on the client and server!

There are no hidden DOM manipulations or other secrets; it’s just HTML attributes and Svelte stores. No JavaScript is needed for the basics.

Adding progressive enhancement

As a last step, let’s add progressive enhancement, so the JS users will have a better experience. It’s also required to use for example client-side validation and events, and of course to avoid reloading the page when the form is posted.

This is simply done with enhance, returned from superForm:

<script lang="ts">
  const { form, errors, constraints, enhance } = superForm(data.form);
  //                                 ^^^^^^^
</script>

<!-- Add to the form element: -->
<form method="POST" use:enhance>

Now the page won’t fully reload when submitting, and we unlock lots of client-side features like timers for loading spinners, auto error focus, tainted fields, etc, that you can read about under the Concepts section in the navigation.

The use:enhance action takes no arguments; instead, events are used to hook into the SvelteKit use:enhance parameters and more. Check out the events page for details.

Next steps

This concludes the tutorial! To learn the details, keep reading under the Concepts section in the navigation. Status messages are very common to add, for example. Also, if you plan to use nested data (objects and arrays within the schema), read the nested data page carefully. The same goes for having multiple forms on the same page.

When you’re ready for something more advanced, check out the CRUD tutorial, which shows how to make a fully working backend in about 150 lines of code.