This episode covers form handling and validation in the Svelte ecosystem: bindings and events for inputs, validation patterns and custom validators, server-side validation with SvelteKit form actions, and accessible forms with good UX feedback.

Forms are the main data entry point for almost every application: login, registration, checkout, search. Yet forms are also one of the biggest sources of bugs and user frustration — confusing error messages, lost data, or validation that only half works.
Svelte makes form handling feel natural because its bindings connect state directly to inputs. For validation, there are two complementary layers: client-side for fast feedback and server-side for security. SvelteKit even provides form actions that handle the entire submission flow with a single pattern.
This episode covers bindings and events for forms, validation patterns and custom validators, server-side validation with SvelteKit form actions, and accessibility and UX feedback. You will leave with a form pattern you can use for any need.
The most direct way to bind an input to state is bind:value — a pattern you already know from episode 4:
<script>
let nama = $state("")
let email = $state("")
function submit(event) {
event.preventDefault()
console.log({ nama, email })
}
</script>
<form onsubmit={submit}>
<input bind:value={nama} placeholder="Nama" />
<input bind:value={email} type="email" placeholder="Email" />
<button type="submit">Kirim</button>
</form>bind:value={nama} keeps state in sync with the input's content without writing a manual oninput. event.preventDefault() stops the form's default page reload. After submit, all values are already in state — no DOM querying needed.
Basic validation can be done at submit time or as the input changes:
<script>
let email = $state("")
let error = $state("")
function submit(event) {
event.preventDefault()
if (!email.includes("@")) {
error = "Format email tidak valid"
return
}
error = ""
console.log("Valid!", email)
}
</script>
<form onsubmit={submit}>
<input bind:value={email} type="email" placeholder="Email" />
{#if error}
<p class="error" role="alert">{error}</p>
{/if}
<button type="submit">Kirim</button>
</form>if (!email.includes("@")) is minimal validation. The error message is stored in state and displayed with role="alert". Client-side validation gives instant feedback without waiting for the network — but never rely on it as your only layer of defense.
Wrap validation rules into functions so they can be reused and tested:
export function validasiEmail(value) {
const pola = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return pola.test(value) ? null : "Alamat email tidak valid"
}
export function validasiWajib(value) {
return value.trim().length > 0 ? null : "Field ini wajib diisi"
}pola.test(value) checks the email format with a regex. Each validator returns an error message, or null when it passes. This consistent structure makes composing validators easy — you can combine several rules for a single field.
SvelteKit handles form submission without JavaScript using form actions. A +page.server.js file exports an action named default:
import { fail } from "@sveltejs/kit"
export const actions = {
default: async ({ request }) => {
const data = await request.formData()
const email = String(data.get("email") ?? "")
if (!email.includes("@")) {
return fail(400, { email, error: "Email tidak valid" })
}
return { sukses: true }
},
}request.formData() reads the form data on the server. fail(400, { email, error }) returns a 400 response along with the error message and the values the user typed — so the fields do not go empty after an error. This is server-side validation: nobody can skip it.
The page component receives the action result through the form prop:
<script>
let { form } = $props()
</script>
<form method="POST" action="?/default">
<input name="email" type="email" value={form?.email ?? ""} />
{#if form?.error}
<p role="alert">{form.error}</p>
{/if}
<button type="submit">Kirim</button>
</form>let { form } = $props() holds the result of the last action. form?.error displays the server error message, and value={form?.email ?? ""} preserves the input content. The form works fully without JavaScript, and automatically becomes progressive when JS loads.
Accessible forms start with correct labels. Every input must have a <label> — or an aria-label attribute if a visual label is impossible:
<form>
<label for="nama">Nama lengkap</label>
<input id="nama" name="nama" />
<label for="umur">Umur</label>
<input id="umur" name="umur" type="number" min="0" max="120" />
<button type="submit">Simpan</button>
</form>label for="nama" connects the label to the input through the id attribute. Screen readers announce the label when the user focuses the input. The min and max attributes provide built-in browser validation as well as guidance for keyboard users.
Good form feedback prevents frustration:
aria-invalid="true" on a failed input helps assistive technologies flag the error. Combined with an error message linked via aria-describedby, screen reader users get the same context as visual users.
Key takeaways:
bind:value connects inputs to state; FormData grabs all fields at once.event.preventDefault() for JavaScript-handled forms.fail() returns input values along with errors so fields do not go empty after a failure.role="alert", and clear feedback make forms accessible and comfortable.In the next episode 11 we will discuss configuration and environment — environment variables and runtime config, Vite and SvelteKit configuration, asset management and static file handling, and feature flags and multi-environment setup. Your forms will talk to a well-managed production environment.