Skip to content
All essays
WebMarch 27, 202510 min

Building Forms in Svelte

Master form handling in Svelte. Learn validation, error handling, and form best practices.

Ü
Ümit Uz
Mobile & Full Stack Developer

Svelte makes form handling simple with its two-way binding and reactive declarations.

Basic Form Handling

svelte
<script>
  let name = '';
  let email = '';
  let message = '';

  function handleSubmit() {
    console.log({ name, email, message });
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <label>
    Name:
    <input type="text" bind:value={name} />
  </label>

  <label>
    Email:
    <input type="email" bind:value={email} />
  </label>

  <label>
    Message:
    <textarea bind:value={message}></textarea>
  </label>

  <button type="submit">Submit</button>
</form>

Form Validation

svelte
<script>
  let formData = {
    name: '',
    email: '',
    password: ''
  };

  let errors = {};

  function validate() {
    errors = {};

    if (!formData.name) {
      errors.name = 'Name is required';
    }

    if (!formData.email) {
      errors.email = 'Email is required';
    } else if (!/^[^@]+@[^@]+.[^@]+$/.test(formData.email)) {
      errors.email = 'Invalid email';
    }

    if (!formData.password) {
      errors.password = 'Password is required';
    } else if (formData.password.length < 8) {
      errors.password = 'Password must be at least 8 characters';
    }

    return Object.keys(errors).length === 0;
  }

  function handleSubmit() {
    if (validate()) {
      console.log('Form submitted:', formData);
    }
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <label>
    Name:
    <input type="text" bind:value={formData.name} />
    {#if errors.name}
      <span class="error">{errors.name}</span>
    {/if}
  </label>

  <label>
    Email:
    <input type="email" bind:value={formData.email} />
    {#if errors.email}
      <span class="error">{errors.email}</span>
    {/if}
  </label>

  <label>
    Password:
    <input type="password" bind:value={formData.password} />
    {#if errors.password}
      <span class="error">{errors.password}</span>
    {/if}
  </label>

  <button type="submit">Submit</button>
</form>

Checkbox and Radio Groups

svelte
<script>
  let isChecked = false;
  let selectedOption = 'option1';

  let toppings = [];
  let allToppings = ['Pepperoni', 'Mushrooms', 'Onions', 'Sausage'];
</script>

<label>
  <input type="checkbox" bind:checked={isChecked} />
  Accept terms and conditions
</label>

<div>
  {#each ['option1', 'option2', 'option3'] as option}
    <label>
      <input type="radio" bind:group={selectedOption} value={option} />
      {option}
    </label>
  {/each}
</div>

<div>
  {#each allToppings as topping}
    <label>
      <input type="checkbox" bind:group={toppings} value={topping} />
      {topping}
    </label>
  {/each}
</div>

Select and Multi-Select

svelte
<script>
  let selected = 'apple';
  let fruits = ['apple', 'banana', 'orange', 'grape'];

  let selectedItems = [];
  let allItems = [
    { value: 'item1', label: 'Item 1' },
    { value: 'item2', label: 'Item 2' },
    { value: 'item3', label: 'Item 3' }
  ];
</script>

<select bind:value={selected}>
  {#each fruits as fruit}
    <option value={fruit}>{fruit}</option>
  {/each}
</select>

<select multiple bind:value={selectedItems}>
  {#each allItems as item}
    <option value={item.value}>{item.label}</option>
  {/each}
</select>

File Uploads

svelte
<script>
  let files;

  async function handleUpload() {
    const formData = new FormData();

    for (const file of files) {
      formData.append('files', file);
    }

    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData
    });

    const result = await response.json();
    console.log(result);
  }
</script>

<form on:submit|preventDefault={handleUpload}>
  <label>
    Upload files:
    <input type="file" bind:files={files} multiple />
  </label>

  {#if files}
    <p>Selected {files.length} file(s)</p>
  {/if}

  <button type="submit">Upload</button>
</form>

Form Stores

javascript
// stores/form.js
import { writable, derived } from 'svelte/store';

function createForm(initial, validate) {
  const form = writable(initial);
  const errors = writable({});
  const touched = writable({});

  const isValid = derived([form, errors], ([$form, $errors]) => {
    return Object.keys($errors).length === 0;
  });

  function touch(field) {
    touched.update(t => ({ ...t, [field]: true }));
  }

  async function submit(onSubmit) {
    const validationErrors = await validate(get(form));

    if (Object.keys(validationErrors).length > 0) {
      errors.set(validationErrors);
      return false;
    }

    errors.set({});
    return onSubmit(get(form));
  }

  function reset() {
    form.set(initial);
    errors.set({});
    touched.set({});
  }

  return {
    form,
    errors,
    touched,
    isValid,
    touch,
    submit,
    reset
  };
}

Form Actions

javascript
// actions/form.js
export function formEnhance(node, { onSubmit, validate }) {
  async function handleSubmit(event) {
    event.preventDefault();

    const formData = new FormData(event.target);
    const data = Object.fromEntries(formData);

    if (validate) {
      const errors = await validate(data);
      if (errors) {
        node.dispatchEvent(new CustomEvent('errors', { detail: errors }));
        return;
      }
    }

    await onSubmit(data);
  }

  node.addEventListener('submit', handleSubmit);

  return {
    destroy() {
      node.removeEventListener('submit', handleSubmit);
    }
  };
}

Svelte provides a simple and powerful way to handle forms with minimal boilerplate.

Related essays

Next essay
Swift Fundamentals: Getting Started with Modern iOS Development