Multiple forms on the same page

Since there is only one $page store per route, multiple forms on the same page, like a register and login form, can cause problems since both form actions will update $page.form, possibly affecting the other form.

With Superforms, multiple forms on the same page are handled automatically if you are using use:enhance, and the forms have different schema types. When using the same schema for multiple forms, you need to set the id option:

const form = await superValidate(zod(schema), {
  id: string | undefined
});

By setting an id on the server, you’ll ensure that only forms with the matching id on the client will react to the updates.

Here’s an example of how to handle a login and registration form on the same page:

+page.server.ts

import { z } from 'zod';
import { fail } from '@sveltejs/kit';
import { message, superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import type { Actions, PageServerLoad } from './$types';

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  confirmPassword: z.string().min(8)
});

export const load: PageServerLoad = async () => {
  // Different schemas, no id required.
  const loginForm = await superValidate(zod(loginSchema));
  const registerForm = await superValidate(zod(registerSchema));

  // Return them both
  return { loginForm, registerForm };
};

export const actions = {
  login: async ({ request }) => {
    const loginForm = await superValidate(request, zod(loginSchema));

    if (!loginForm.valid) return fail(400, { loginForm });

    // TODO: Login user
    return message(loginForm, 'Login form submitted');
  },

  register: async ({ request }) => {
    const registerForm = await superValidate(request, zod(registerSchema));

    if (!registerForm.valid) return fail(400, { registerForm });

    // TODO: Register user
    return message(registerForm, 'Register form submitted');
  }
} satisfies Actions;

The code above uses named form actions to determine which form was posted. On the client, you’ll post to these different form actions for the respective form:

+page.svelte

<script lang="ts">
  import { superForm } from 'sveltekit-superforms/client';
  import type { PageData } from './$types.js';

  export let data: PageData;

  const { form, errors, enhance, message } = superForm(data.loginForm, {
    resetForm: true
  });

  const {
    form: registerForm,
    errors: registerErrors,
    enhance: registerEnhance,
    message: registerMessage
  } = superForm(data.registerForm, {
    resetForm: true
  });
</script>

{#if $message}<h3>{$message}</h3>{/if}

<form method="POST" action="?/login" use:enhance>
  E-mail: <input name="email" type="email" bind:value={$form.email} />
  Password:
  <input name="password" type="password" bind:value={$form.password} />
  <button>Submit</button>
</form>

<hr />

{#if $registerMessage}<h3>{$registerMessage}</h3>{/if}

<form method="POST" action="?/register" use:registerEnhance>
  E-mail: <input name="email" type="email" bind:value={$registerForm.email} />
  Password:
  <input name="password" type="password" bind:value={$registerForm.password} />
  Confirm password:
  <input
    name="confirmPassword"
    type="password"
    bind:value={$registerForm.confirmPassword} />
  <button>Submit</button>
</form>

The above works well with forms that posts to a dedicated form action. But for more dynamic scenarios, let’s say a database table where rows can be edited, the form id should correspond to the row id, and you’d want to communicate to the server which id was sent. This can be done by modifying the $formId store, to let the server know what id was posted, and what it should respond with.

Setting id on the client

On the client, the id is picked up automatically when you pass data.form to superForm, so in general, you don’t have to add it on the client.

// Default behavior: The id is sent along with the form data
// sent from the load function.
const { form, enhance, formId } = superForm(data.loginForm);

// In advanced cases, you can set the id as an option
// and it will take precedence.
const { form, enhance, formId } = superForm(data.form, {
  id: 'custom-id'
});

You can also change the id dynamically with the $formId store, or set it directly in the form with the following method:

Without use:enhance

Multiple forms also work without use:enhance, though in this case you must add a hidden form field called __superform_id with the $formId value:

<script lang="ts">
  import { superForm } from 'sveltekit-superforms/client';
  import type { PageData } from './$types.js';

  export let data: PageData;

  const { form, errors, formId } = superForm(data.form);
</script>

<form method="POST" action="?/login">
  <input type="hidden" name="__superform_id" bind:value={$formId} />
</form>

This is also required if you’re changing schemas in a form action, as can happen in multi-step forms.

Returning multiple forms

If you have a use case where the data in one form should update another, you can return both forms in the form action: return { loginForm, registerForm }, but be aware that you may need resetForm: false on the second form, as it will reset and clear the updated changes, if it’s valid and a successful response is returned.

Hidden forms

Sometimes you want a fetch function for a form field or a list of items, for example checking if a username is valid while entering it, or deleting rows in a list of data. Instead of doing this manually with fetch, which cannot take advantage of Superforms’ loading timers, events and other functionality, you can create a hidden form that does most of the work, with the convenience you get from superForm:

// First the visible form
const { form, errors, ... } = superForm(...);

// The the hidden form
const { submitting, submit } = superForm(
  { username: '' },
  {
    invalidateAll: false,
    applyAction: false,
    multipleSubmits: 'abort',
    onSubmit({ cancel, formData }) {
      // Using the visible form data
      if (!$form.username) cancel();
      formData.set('username', $form.username);
    },
    onUpdated({ form }) {
      // Update the other form to show the error message
      $errors.username = form.errors.username;
    }
  }
);

const checkUsername = debounce(300, submit);

Create a form action for it:

const usernameSchema = fullSchema.pick({ username: true });

export const actions: Actions = {
  check: async ({ request }) => {
    const form = await superValidate(request, zod(usernameSchema));

    if (!form.valid) return fail(400, { form });
    
    if(!checkUsername(form.data.username)) {
      setError(form, 'username', 'Username is already taken.');
    }

    return { form };
  }
};

And finally, an on:input event on the input field:

<input
  name="username"
  aria-invalid={$errors.username ? 'true' : undefined}
  bind:value={$form.username}
  on:input={checkUsername}
/>
{#if $submitting}<img src={spinner} alt="Checking availability" />
{:else if $errors.username}<div class="invalid">{$errors.username}</div>{/if}

A full example of a username check is available on SvelteLab.

Configuration and troubleshooting

Due to the many different use cases, it’s hard to set sensible default options for multiple forms. A common issue is that when one form is submitted, the other forms’ data are lost. This is due to the page being invalidated by default on a successful response.

If you want to preserve their data, you’d almost certainly want to set invalidateAll: false or applyAction: false on them, as in the example above. The use:enhance option explains the differences between them.

Also check out the componentization page for ideas on how to place the forms into separate components, to make +page.svelte less cluttered.

Test it out

Here we have two forms, with separate id’s and their invalidateAll option set to false, to prevent page invalidation, which would otherwise clear the other form when the load function executes again.

Register user

Login

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