Skip to main content

Form Patterns

Form Patterns

Best practices and patterns for building forms

Tags: pattern forms validation

This guide covers best practices and common patterns for building forms with the theme library.

Basic Form Structure​

<script>
  import { FormField, Input, Button, toastStore } from '@classic-homes/theme-svelte';

  let formData = $state({
    name: '',
    email: '',
  });

  let errors = $state({});
  let submitting = $state(false);

  async function handleSubmit(e) {
    e.preventDefault();

    if (!validate()) return;

    submitting = true;
    try {
      await submitForm(formData);
      toastStore.success('Form submitted successfully');
    } catch (error) {
      toastStore.error('Failed to submit form');
    } finally {
      submitting = false;
    }
  }

  function validate() {
    errors = {};
    if (!formData.name) errors.name = 'Name is required';
    if (!formData.email) errors.email = 'Email is required';
    return Object.keys(errors).length === 0;
  }
</script>

<form onsubmit={handleSubmit} class="space-y-4">
  <FormField id="name" label="Name" error={errors.name} required>
    <Input id="name" bind:value={formData.name} error={!!errors.name} />
  </FormField>

  <FormField id="email" label="Email" error={errors.email} required>
    <Input id="email" type="email" bind:value={formData.email} error={!!errors.email} />
  </FormField>

  <Button type="submit" disabled={submitting}>
    {submitting ? 'Submitting...' : 'Submit'}
  </Button>
</form>

Form Layout Patterns​

Single Column​

<form class="max-w-md space-y-4">
  <Input label="Field 1" />
  <Input label="Field 2" />
  <Input label="Field 3" />
</form>

Two Column Grid​

<form class="space-y-4">
  <div class="grid grid-cols-2 gap-4">
    <Input label="First Name" />
    <Input label="Last Name" />
  </div>
  <Input label="Email" type="email" />
  <div class="grid grid-cols-2 gap-4">
    <Input label="City" />
    <Input label="State" />
  </div>
</form>

Responsive Grid​

<form class="space-y-4">
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
    <Input label="First Name" />
    <Input label="Last Name" />
  </div>
  <Input label="Email" type="email" />
</form>

Validation Patterns​

Real-time Validation​

<script>
  let email = $state('');
  let emailError = $state('');
  let emailTouched = $state(false);

  $effect(() => {
    if (!emailTouched) return;

    if (!email) {
      emailError = 'Email is required';
    } else if (!email.includes('@')) {
      emailError = 'Please enter a valid email';
    } else {
      emailError = '';
    }
  });
</script>

<Input
  label="Email"
  type="email"
  bind:value={email}
  error={emailError}
  onblur={() => (emailTouched = true)}
/>

Submit Validation​

<script>
  function validateForm() {
    const errors = {};

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

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

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

    return errors;
  }

  function handleSubmit(e) {
    e.preventDefault();
    const validationErrors = validateForm();

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

    // Submit form...
  }
</script>

Schema Validation (with Zod)​

<script>
  import { z } from 'zod';

  const schema = z.object({
    name: z.string().min(1, 'Name is required'),
    email: z.string().email('Invalid email'),
    age: z.number().min(18, 'Must be 18 or older'),
  });

  function validate(data) {
    const result = schema.safeParse(data);
    if (result.success) return {};

    return result.error.flatten().fieldErrors;
  }
</script>

Form State Management​

Controlled Form​

<script>
  let form = $state({
    name: '',
    email: '',
    preferences: {
      newsletter: false,
      notifications: true,
    },
  });
</script>

<Input label="Name" bind:value={form.name} />
<Input label="Email" bind:value={form.email} />
<Checkbox label="Newsletter" bind:checked={form.preferences.newsletter} />
<Switch label="Notifications" bind:checked={form.preferences.notifications} />

Reset Form​

<script>
  const initialState = {
    name: '',
    email: '',
  };

  let form = $state({ ...initialState });

  function reset() {
    form = { ...initialState };
    errors = {};
  }
</script>

<form>
  <!-- fields -->
  <div class="flex gap-2">
    <Button type="submit">Submit</Button>
    <Button type="button" variant="outline" onclick={reset}>Reset</Button>
  </div>
</form>

Dirty State Tracking​

<script>
  const initialState = { name: '', email: '' };
  let form = $state({ ...initialState });

  const isDirty = $derived(JSON.stringify(form) !== JSON.stringify(initialState));
</script>

<Button type="submit" disabled={!isDirty}>Save Changes</Button>

Common Form Types​

Login Form​

<form class="space-y-4" onsubmit={handleLogin}>
  <Input label="Email" type="email" bind:value={email} required autocomplete="email" />
  <Input
    label="Password"
    type="password"
    bind:value={password}
    required
    autocomplete="current-password"
  />
  <div class="flex items-center justify-between">
    <Checkbox label="Remember me" bind:checked={rememberMe} />
    <a href="/forgot-password" class="text-sm text-primary"> Forgot password? </a>
  </div>
  <Button type="submit" class="w-full" disabled={loading}>
    {loading ? 'Signing in...' : 'Sign In'}
  </Button>
</form>

Registration Form​

<form class="space-y-4" onsubmit={handleRegister}>
  <div class="grid grid-cols-2 gap-4">
    <Input label="First Name" bind:value={firstName} required />
    <Input label="Last Name" bind:value={lastName} required />
  </div>
  <Input label="Email" type="email" bind:value={email} required />
  <Input label="Password" type="password" bind:value={password} required />
  <Input
    label="Confirm Password"
    type="password"
    bind:value={confirmPassword}
    error={password !== confirmPassword ? 'Passwords do not match' : ''}
    required
  />
  <Checkbox required>
    I agree to the <a href="/terms">Terms of Service</a>
  </Checkbox>
  <Button type="submit" class="w-full">Create Account</Button>
</form>

Contact Form​

<form class="space-y-4">
  <Input label="Name" bind:value={name} required />
  <Input label="Email" type="email" bind:value={email} required />
  <Select label="Subject" bind:value={subject} required>
    <option value="">Select a subject...</option>
    <option value="sales">Sales Inquiry</option>
    <option value="support">Technical Support</option>
    <option value="other">Other</option>
  </Select>
  <Textarea label="Message" bind:value={message} rows={5} required />
  <Button type="submit">Send Message</Button>
</form>

Multi-Step Forms​

<script>
  let step = $state(1);
  const totalSteps = 3;

  function nextStep() {
    if (validateStep(step)) {
      step = Math.min(step + 1, totalSteps);
    }
  }

  function prevStep() {
    step = Math.max(step - 1, 1);
  }
</script>

<div class="space-y-6">
  <!-- Progress indicator -->
  <div class="flex justify-between">
    {#each Array(totalSteps) as _, i}
      <div class={cn('h-2 flex-1 mx-1 rounded', i < step ? 'bg-primary' : 'bg-muted')} />
    {/each}
  </div>

  <!-- Step content -->
  {#if step === 1}
    <div class="space-y-4">
      <h2>Personal Information</h2>
      <Input label="Name" bind:value={form.name} />
      <Input label="Email" bind:value={form.email} />
    </div>
  {:else if step === 2}
    <div class="space-y-4">
      <h2>Address</h2>
      <Input label="Street" bind:value={form.street} />
      <Input label="City" bind:value={form.city} />
    </div>
  {:else}
    <div class="space-y-4">
      <h2>Review</h2>
      <!-- Review content -->
    </div>
  {/if}

  <!-- Navigation -->
  <div class="flex justify-between">
    <Button variant="outline" onclick={prevStep} disabled={step === 1}>Previous</Button>
    {#if step < totalSteps}
      <Button onclick={nextStep}>Next</Button>
    {:else}
      <Button onclick={handleSubmit}>Submit</Button>
    {/if}
  </div>
</div>

Accessibility​

  • Always use <label> or FormField for inputs
  • Include proper autocomplete attributes
  • Mark required fields clearly
  • Announce errors to screen readers
  • Maintain logical tab order
On this page