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
bcryptorargon2, 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
Originheader 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 →