SvelteKit Signup Form with Email and Password Validation

Building a Production Signup Form in SvelteKit

Signup forms have more validation complexity than simple contact forms: password strength rules, password confirmation, email uniqueness checks, and terms acceptance. Here’s how to build one properly with Superforms and Zod.

The Schema: More Than Just Required Fields

Signup schemas need cross-field validation (password confirmation) and compound rules (password strength). Zod handles both elegantly:

import { z } from 'zod';

export const signupSchema = z.object({
  name: z.string()
    .min(2, 'Name must be at least 2 characters'),
  email: z.string()
    .email('Please enter a valid email address'),
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Must contain at least one uppercase letter')
    .regex(/[a-z]/, 'Must contain at least one lowercase letter')
    .regex(/[0-9]/, 'Must contain at least one number'),
  confirmPassword: z.string(),
  terms: z.boolean()
    .refine(val => val === true, 'You must accept the terms')
}).refine(
  data => data.password === data.confirmPassword,
  { message: 'Passwords do not match', path: ['confirmPassword'] }
);

export type SignupSchema = typeof signupSchema;

What Makes This Schema Production-Ready

The password field chains three .regex() validators. Each produces its own error message, so users see all unmet requirements at once — not one at a time. This is better UX than a single “password too weak” message.

The .refine() at the object level compares password and confirmPassword. The path: ['confirmPassword'] option makes the error appear on the confirm field, not the password field.

The terms checkbox uses .refine(val => val === true) because z.boolean() alone accepts both true and false. You need the refinement to require explicit acceptance.

The Server Action

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

export const load: PageServerLoad = async () => {
  const form = await superValidate(zod(signupSchema));
  return { form };
};

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

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

    // Check if email already exists
    const existingUser = await db.user.findUnique({
      where: { email: form.data.email }
    });

    if (existingUser) {
      return message(form, 'An account with this email already exists', {
        status: 400
      });
    }

    // Hash password and create user
    const hashedPassword = await hashPassword(form.data.password);
    await db.user.create({
      data: {
        name: form.data.name,
        email: form.data.email,
        password: hashedPassword
      }
    });

    return message(form, 'Account created successfully!');
  }
};

Two things the server does that the client cannot: email uniqueness checking (requires a database query) and password hashing (must happen server-side, never in the browser). This is why server-side validation isn’t optional — it handles security-critical logic.

The Form Component

import { superForm } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { signupSchema } from './schema';
import type { PageData } from './$types';

let { data } = $props();

const { form, errors, enhance, submitting } = superForm(data.form, {
  validators: zod(signupSchema)
});

Template with Password Strength Feedback

<form method="POST" use:enhance class="space-y-4 max-w-md">
  <div>
    <label for="name" class="block text-sm font-medium">Full Name</label>
    <input id="name" name="name" bind:value={$form.name}
      class="mt-1 block w-full rounded-md border px-3 py-2" />
    {#if $errors.name}
      <p class="mt-1 text-sm text-red-600">{$errors.name}</p>
    {/if}
  </div>

  <div>
    <label for="email" class="block text-sm font-medium">Email</label>
    <input id="email" name="email" type="email" bind:value={$form.email}
      class="mt-1 block w-full rounded-md border px-3 py-2" />
    {#if $errors.email}
      <p class="mt-1 text-sm text-red-600">{$errors.email}</p>
    {/if}
  </div>

  <div>
    <label for="password" class="block text-sm font-medium">Password</label>
    <input id="password" name="password" type="password" bind:value={$form.password}
      class="mt-1 block w-full rounded-md border px-3 py-2" />
    {#if $errors.password}
      <p class="mt-1 text-sm text-red-600">{$errors.password}</p>
    {/if}
  </div>

  <div>
    <label for="confirm" class="block text-sm font-medium">Confirm Password</label>
    <input id="confirm" name="confirmPassword" type="password"
      bind:value={$form.confirmPassword}
      class="mt-1 block w-full rounded-md border px-3 py-2" />
    {#if $errors.confirmPassword}
      <p class="mt-1 text-sm text-red-600">{$errors.confirmPassword}</p>
    {/if}
  </div>

  <div class="flex items-center gap-2">
    <input id="terms" name="terms" type="checkbox" bind:checked={$form.terms} />
    <label for="terms" class="text-sm">I agree to the Terms of Service</label>
  </div>
  {#if $errors.terms}
    <p class="text-sm text-red-600">{$errors.terms}</p>
  {/if}

  <button type="submit" disabled={$submitting}
    class="w-full rounded-md bg-blue-600 px-4 py-2 text-white disabled:opacity-50">
    {$submitting ? 'Creating account...' : 'Create Account'}
  </button>
</form>

Security Considerations

A few things to keep in mind for production signup forms:

  • Rate limiting — protect against brute-force registration. SvelteKit doesn’t have built-in rate limiting, but you can add it with a middleware or at the Cloudflare/Vercel edge layer.
  • Email verification — don’t trust unverified emails. Send a confirmation link before activating the account.
  • Password hashing — use bcrypt or argon2, never store plaintext. The hashing must happen server-side in the form action, never in the browser.
  • CSRF protection — SvelteKit form actions include CSRF protection by default via the Origin header check.

Generate This Form Instantly

SvelteForms has a Sign Up template that generates all three files with the exact patterns shown above. Load it in the form builder, customize the fields, and export a production-ready route folder.

For more validation patterns, see our guide on Zod schema patterns including password strength, conditional fields, and cross-field validation.

Build forms faster

Try the form builder →