Learn Svelte - Forms & Validation
Series/Learn Svelte/Episode 10
Episode 10 of 24

Learn Svelte - Forms & Validation

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.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

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.

Form Handling with Binding and Events

One-to-One State with Inputs

The most direct way to bind an input to state is bind:value — a pattern you already know from episode 4:

Form with binding
<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.

Validation Patterns and Custom Validators

Simple Client-Side Validation

Basic validation can be done at submit time or as the input changes:

Validation on submit
<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.

Reusable Custom Validators

Wrap validation rules into functions so they can be reused and tested:

JSReusable validator
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.

Server-Side Validation in SvelteKit

Form Actions

SvelteKit handles form submission without JavaScript using form actions. A +page.server.js file exports an action named default:

JSForm action with server validation
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.

Displaying the Action Result in the Page

The page component receives the action result through the form prop:

Page displaying the server error
<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.

Accessibility and UX Feedback

Labels and Focus

Accessible forms start with correct labels. Every input must have a <label> — or an aria-label attribute if a visual label is impossible:

Form with accessible labels
<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 UX Feedback

Good form feedback prevents frustration:

  • Validate immediately when a field is left, not only on submit.
  • Error messages name the offending field and how to fix it.
  • Disable the submit button while the form is processing.
  • Do not clear the values the user has typed when an error occurs.

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.

Conclusion

Key takeaways:

  • bind:value connects inputs to state; FormData grabs all fields at once.
  • Always call event.preventDefault() for JavaScript-handled forms.
  • Reusable validators return an error message or null, and can be composed.
  • Server-side validation with SvelteKit form actions cannot be skipped and works without JS.
  • fail() returns input values along with errors so fields do not go empty after a failure.
  • Correct labels, 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.