Skip to content

Forms & Inputs

Forms bring bindings, runes, and events together. Each field binds to state; validation derives from that state; submit reads it. Here’s the shape of a real form:

<script>
let email = $state('');
let password = $state('');
// Validation is DERIVED from the fields — never stored separately.
let emailValid = $derived(/^[^@]+@[^@]+\.[^@]+$/.test(email));
let passwordValid = $derived(password.length >= 8);
let formValid = $derived(emailValid && passwordValid);
</script>
<input type="email" bind:value={email} placeholder="Email" />
{#if email && !emailValid}<small>Enter a valid email</small>{/if}
<input type="password" bind:value={password} placeholder="Password" />
{#if password && !passwordValid}<small>At least 8 characters</small>{/if}

The key idea: validation is derived, not stored. emailValid recomputes automatically whenever email changes — there’s no “revalidate” step to forget. This is the “derive, don’t sync” principle from the effects lesson, applied to forms.

Attach onsubmit to the <form> and call e.preventDefault() to stop the browser’s native navigation, then act on the collected state:

<script>
let submitting = $state(false);
async function handleSubmit(e) {
e.preventDefault(); // Svelte 5 has no on:submit|preventDefault modifier
if (!formValid) return;
submitting = true;
await createAccount({ email, password });
submitting = false;
}
</script>
<form onsubmit={handleSubmit}>
<!-- fields… -->
<button type="submit" disabled={!formValid || submitting}>
{submitting ? 'Creating…' : 'Sign up'}
</button>
</form>

Note the disabled button derives from formValid and submitting — the UI stays correct without any manual toggling.

flowchart LR
  input["inputs (bind:value)"] --> state["field state"]
  state --> valid["$derived validation"]
  valid --> button["submit button enabled/disabled"]
  button --> submit["onsubmit → preventDefault → act"]
A form: bind, derive, submit

This lesson handles forms entirely on the client — fine for local state and simple cases. But real apps need the data on a server: persistence, auth, validation you can trust. That’s what SvelteKit form actions (a later module) provide — progressive-enhancement forms that work with and without JavaScript, validated on the server. Client-side bind: + $derived gives you responsive UX; SvelteKit gives you the full-stack pipeline.

How should form validation state be managed?
How do you prevent the browser navigating on form submit in Svelte 5?
Why derive the submit button's `disabled` value?
What does client-side form handling NOT give you?