Get started
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 tutorial on SvelteLab to follow along in the browser and copy the code from there.
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, pipe, minLength } from 'valibot';
export const schema = object({
name: pipe(optional(string(), 'Hello world!'), minLength(2)),
email: pipe(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, you import an adapter corresponding to your library of choice, together with the schema, and pass it in a load function to the superValidate
function:
src/routes/+page.server.ts
import { superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import { object, string, email, optional, pipe, minLength } from 'valibot';
// Define outside the load function so the adapter can be cached
const schema = object({
name: pipe(optional(string(), 'Hello world!'), minLength(2)),
email: pipe(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:
import { error } from '@sveltejs/kit';
export const load = async ({ params }) => {
// Replace with your database
const user = db.users.findUnique({
where: { id: params.id }
});
if (!user) error(404, 'Not found');
const form = await superValidate(user, your_adapter(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 a 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 look behind the scenes:
src/routes/+page.svelte
<script lang="ts">
import SuperDebug from 'sveltekit-superforms';
</script>
<SuperDebug data={$form} />
This should be displayed:
undefined
When editing the form fields (try in the form above), the data is automatically updated. The component also displays a copy button and the current page status in the right corner. There are many configuration options available.
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 containsFormData
) - Use the
event
object (which contains the request) - Use
FormData
directly, if you need to access it before callingsuperValidate
.
The most common is to use request
:
src/routes/+page.server.ts
import { message } from 'sveltekit-superforms';
import { fail } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, your_adapter(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
// Display a success status message
return message(form, 'Form posted successfully!');
}
};
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, 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, message } = superForm(data.form);
// ^^^^^^ ^^^^^^^^^^^ ^^^^^^^
</script>
{#if $message}<h3>{$message}</h3>{/if}
<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>
By including the errors
store, we can display errors where appropriate, and through constraints
we’ll get browser validation even without JavaScript enabled. The aria-invalid
attribute is used to automatically focus on the first error field. And finally, we added a status message above the form to show if it was posted successfully.
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, so it works with server-side rendering. No JavaScript is required for the basics.
Adding progressive enhancement
As a last step, let’s add progressive enhancement, so users with JavaScript enabled will have a nicer experience. It’s also required for enabling 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, message, 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, which 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. A status message is 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.
Enjoy your Superforms!