| name | valibot-superforms |
| description | Form validation for Velociraptor using Valibot v1 + Superforms. Use when creating forms, validation schemas, handling errors, or implementing accessible form patterns. Includes Svelte 5 patterns, progressive enhancement, file uploads, and WCAG accessibility. Essential for any form implementation. |
Valibot + Superforms
Type-safe form validation with tree-shakeable schemas. Valibot v1 + Superforms v2.
Contents
- Quick Start - Schema, server, client setup
- Valibot Schemas - Types, pipe(), objects, arrays
- Superforms Stores - form, errors, submitting, etc.
- Error Display - Field errors, summary, server-side
- Loading States - submitting vs delayed
- Configuration - Validation, error handling, reset
- Events - onSubmit, onUpdate, onError
- Anti-Patterns - Common mistakes
- References - Detailed guides
| Concept | Purpose |
|---|
| Valibot schemas | Define validation rules |
pipe() | Chain validations and transformations |
| Superforms | SvelteKit form state management |
valibotClient | Client-side validation adapter |
| Progressive enhancement | Works without JS |
Quick Start
Schema Definition
import * as v from 'valibot';
export const loginSchema = v.object({
email: v.pipe(v.string(), v.email('Invalid email')),
password: v.pipe(v.string(), v.minLength(8, 'Min 8 characters'))
});
export type LoginInput = v.InferInput<typeof loginSchema>;
export type LoginOutput = v.InferOutput<typeof loginSchema>;
Server Setup
import { superValidate, fail, message } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import { loginSchema } from '$lib/schemas/auth';
export const load = async () => {
const form = await superValidate(valibot(loginSchema));
return { form };
};
export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, valibot(loginSchema));
if (!form.valid) {
return fail(400, { form });
}
await authenticate(form.data.email, form.data.password);
return message(form, 'Login successful!');
}
};
Client Setup (Svelte 5)
<script lang="ts">
import { superForm } from 'sveltekit-superforms';
import { valibotClient } from 'sveltekit-superforms/adapters';
import { loginSchema } from '$lib/schemas/auth';
let { data } = $props();
const { form, errors, constraints, enhance, submitting } = superForm(data.form, {
validators: valibotClient(loginSchema)
});
</script>
<form method="POST" use:enhance>
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
bind:value={$form.email}
aria-invalid={$errors.email ? 'true' : undefined}
{...$constraints.email}
/>
{#if $errors.email}
<span class="error">{$errors.email}</span>
{/if}
<label for="password">Password</label>
<input
id="password"
name="password"
type="password"
bind:value={$form.password}
aria-invalid={$errors.password ? 'true' : undefined}
{...$constraints.password}
/>
{#if $errors.password}
<span class="error">{$errors.password}</span>
{/if}
<button disabled={$submitting}>
{$submitting ? 'Logging in...' : 'Login'}
</button>
</form>
Valibot Schemas
Basic Types
import * as v from 'valibot';
const str = v.string();
const num = v.number();
const bool = v.boolean();
const date = v.date();
const email = v.pipe(v.string(), v.email());
const age = v.pipe(v.number(), v.minValue(18));
const url = v.pipe(v.string(), v.url());
The pipe() Function
Chain validations and transformations sequentially:
const EmailSchema = v.pipe(
v.string(),
v.trim(),
v.toLowerCase(),
v.email('Invalid email'),
v.maxLength(100, 'Too long')
);
const PasswordSchema = v.pipe(
v.string(),
v.minLength(8, 'Min 8 characters'),
v.regex(/[A-Z]/, 'Need uppercase'),
v.regex(/[0-9]/, 'Need number')
);
Objects and Arrays
const UserSchema = v.object({
name: v.pipe(v.string(), v.minLength(2)),
email: v.pipe(v.string(), v.email()),
tags: v.array(v.string()),
address: v.object({
street: v.string(),
city: v.string()
})
});
const ProfileSchema = v.object({
bio: v.optional(v.string()),
website: v.nullable(v.string()),
nickname: v.optional(v.string(), '')
});
Transformations
const PixelsSchema = v.pipe(
v.string(),
v.regex(/^\d+px$/),
v.transform((s) => parseInt(s))
);
const ProcessedTags = v.pipe(
v.array(v.string()),
v.filterItems((t) => t.length > 0),
v.mapItems((t) => t.toLowerCase())
);
Custom Validation
const AgeSchema = v.pipe(
v.number(),
v.check((age) => age >= 18, 'Must be 18+')
);
const UniqueEmailSchema = v.pipeAsync(
v.string(),
v.email(),
v.checkAsync(
async (email) => !(await emailExists(email)),
'Email already registered'
)
);
Async Schemas (objectAsync)
When ANY field requires async validation, use objectAsync:
import * as v from 'valibot';
async function isUsernameAvailable(username: string): Promise<boolean> {
const response = await fetch(`/api/check-username?u=${username}`);
const { available } = await response.json();
return available;
}
export const registerSchema = v.objectAsync({
username: v.pipeAsync(
v.string(),
v.minLength(3, 'At least 3 characters'),
v.checkAsync(isUsernameAvailable, 'Username already taken')
),
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export type RegisterInput = v.InferInput<typeof registerSchema>;
Key rules:
- Use
objectAsync if ANY field requires async validation
- Use
pipeAsync + checkAsync for the async field
- Sync fields can use regular
pipe() inside objectAsync
- Increase
delayMs: 500 to reduce server requests
Superforms Stores
const {
form,
errors,
constraints,
message,
submitting,
delayed,
timeout,
tainted,
allErrors,
enhance
} = superForm(data.form, options);
Error Display
Field Errors
<input
name="email"
bind:value={$form.email}
aria-invalid={$errors.email ? 'true' : undefined}
aria-describedby={$errors.email ? 'email-error' : undefined}
/>
{#if $errors.email}
<span id="email-error" class="error" aria-live="polite">
{$errors.email}
</span>
{/if}
Error Summary (Accessibility)
{#if $allErrors.length > 0}
<div role="alert" class="error-summary">
<h2>Please fix {$allErrors.length} error(s):</h2>
<ul>
{#each $allErrors as error}
<li><a href="#{error.path}">{error.messages[0]}</a></li>
{/each}
</ul>
</div>
{/if}
Server-Side Errors
import { setError } from 'sveltekit-superforms';
export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, valibot(schema));
if (await emailExists(form.data.email)) {
return setError(form, 'email', 'Email already registered');
}
return setError(form, 'address.city', 'Invalid city');
}
};
Loading States
<script lang="ts">
const { form, enhance, submitting, delayed } = superForm(data.form, {
delayMs: 500, // When $delayed becomes true
timeoutMs: 8000 // When $timeout becomes true
});
</script>
<button disabled={$submitting}>
{#if $delayed}
<Spinner /> Submitting...
{:else}
Submit
{/if}
</button>
UX Rule: Use $delayed not $submitting for spinners. Operations <500ms don't need visual feedback.
Configuration
const { form, enhance } = superForm(data.form, {
validators: valibotClient(schema),
validationMethod: 'auto',
autoFocusOnError: 'detect',
scrollToError: 'smooth',
errorSelector: '[aria-invalid="true"]',
resetForm: false,
clearOnSubmit: 'errors-and-message',
multipleSubmits: 'prevent',
onSubmit: ({ formData, cancel }) => {},
onResult: ({ result }) => {},
onUpdate: ({ form }) => {},
onError: ({ result }) => {}
});
Events
const { form, enhance } = superForm(data.form, {
onSubmit: ({ formData, cancel }) => {
if (!confirm('Submit?')) cancel();
},
onUpdate: ({ form }) => {
if (form.message) {
toast.success(form.message);
}
},
onError: ({ result, message }) => {
toast.error('Something went wrong');
}
});
Anti-Patterns
Don't use SvelteKit's fail():
import { fail } from '@sveltejs/kit';
return fail(400, { form });
import { fail } from 'sveltekit-superforms';
return fail(400, { form });
Don't skip use:enhance:
<!-- WRONG - no client validation, no events, no timers -->
<form method="POST">
<!-- RIGHT -->
<form method="POST" use:enhance>
Don't validate on every keystroke:
validationMethod: 'oninput'
validationMethod: 'auto'
References
- references/valibot-api.md - Complete Valibot v1 API reference
- references/superforms-config.md - All configuration options
- references/accessibility.md - WCAG form patterns
- references/nested-data.md - Objects, arrays, file uploads
- references/patterns.md - Common form patterns