DEV Community

Lucas Bennett
Lucas Bennett

Posted on

Building Forms with Quaff in Svelte

Quaff is an extensive UI framework for Svelte featuring modern and elegant components that follow Material Design 3 principles. It provides a comprehensive set of pre-built, accessible components that help developers create beautiful, consistent user interfaces quickly. This article focuses on building interactive forms using Quaff's form components, including text inputs, buttons, checkboxes, and validation patterns.

This guide walks through creating production-ready forms using Quaff with Svelte, covering everything from basic form setup to advanced validation and user feedback. This is part 23 of a series on using Quaff with Svelte.

Prerequisites

Before starting, ensure you have:

  • A Svelte project (SvelteKit recommended, but standalone Svelte works too)
  • Node.js 18+ and npm/pnpm/yarn installed
  • Basic familiarity with Svelte components, reactive statements, and event handling
  • Understanding of HTML forms and form validation concepts

For this tutorial, we'll work with a SvelteKit project. If you're using standalone Svelte, the examples will work similarly, but you may need to adjust import paths.

Installation

Install Quaff using your preferred package manager:

npm install quaff
# or
pnpm add quaff
# or
yarn add quaff
Enter fullscreen mode Exit fullscreen mode

The package includes all Quaff components and their TypeScript definitions. If you're using TypeScript, ensure your tsconfig.json has proper module resolution configured.

Project Setup

Quaff requires minimal setup. If you're using SvelteKit, you can start using components immediately. For styling, Quaff uses Material Design 3 tokens, so you may want to include the Material Design 3 CSS variables or use Quaff's built-in theming.

Create a basic layout file to include Quaff's styles (if required by your setup):

<!-- src/app.html or src/routes/+layout.svelte -->
<script>
  // Quaff components work out of the box
  // No additional configuration needed
</script>
Enter fullscreen mode Exit fullscreen mode

If you need to customize the theme, you can add Material Design 3 CSS variables to your global styles:

/* src/app.css or global styles */
:root {
  /* Material Design 3 color tokens */
  --md-sys-color-primary: #6750a4;
  --md-sys-color-on-primary: #ffffff;
  /* Add other tokens as needed */
}
Enter fullscreen mode Exit fullscreen mode

First Example: Basic Form

Let's start with a simple contact form using Quaff components:

<!-- src/routes/contact.svelte -->
<script>
  import { TextField, Button } from 'quaff'

  let formData = {
    name: '',
    email: '',
    message: ''
  }

  function handleSubmit() {
    console.log('Form submitted:', formData)
    // Handle form submission
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <TextField
    label="Name"
    bind:value={formData.name}
    required
  />

  <TextField
    label="Email"
    type="email"
    bind:value={formData.email}
    required
  />

  <TextField
    label="Message"
    bind:value={formData.message}
    multiline
    rows={4}
    required
  />

  <Button type="submit" variant="filled">
    Submit
  </Button>
</form>
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • TextField: Quaff's text input component with Material Design 3 styling
  • Button: Quaff's button component with variant support
  • Two-way binding: Using bind:value to sync form state
  • Form handling: Standard Svelte form submission pattern

The components automatically include Material Design 3 styling, proper accessibility attributes, and focus management.

Understanding Form Components

Quaff provides several form-related components that work together:

TextField Component

The TextField component is the primary input component. It supports various input types and states:

<script>
  import { TextField } from 'quaff'

  let value = ''
  let error = false
</script>

<!-- Basic text field -->
<TextField label="Username" bind:value={value} />

<!-- With helper text -->
<TextField 
  label="Email" 
  type="email"
  helperText="Enter your email address"
  bind:value={value}
/>

<!-- Error state -->
<TextField 
  label="Password"
  type="password"
  error={error}
  errorText={error ? "Password is required" : ""}
  bind:value={value}
/>

<!-- Disabled state -->
<TextField 
  label="Read-only field"
  value="Cannot edit"
  disabled
/>
Enter fullscreen mode Exit fullscreen mode

Checkbox and Radio Components

For boolean choices and single selections:

<script>
  import { Checkbox, Radio, RadioGroup } from 'quaff'

  let termsAccepted = false
  let selectedOption = 'option1'
</script>

<Checkbox 
  label="I agree to the terms and conditions"
  bind:checked={termsAccepted}
/>

<RadioGroup bind:value={selectedOption}>
  <Radio value="option1" label="Option 1" />
  <Radio value="option2" label="Option 2" />
  <Radio value="option3" label="Option 3" />
</RadioGroup>
Enter fullscreen mode Exit fullscreen mode

Practical Example: Registration Form

Let's build a complete registration form with validation and error handling:

<!-- src/routes/register.svelte -->
<script>
  import { TextField, Button, Checkbox } from 'quaff'

  let formData = {
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: '',
    acceptTerms: false
  }

  let errors = {
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: '',
    acceptTerms: ''
  }

  function validateEmail(email) {
    const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return re.test(email)
  }

  function validateForm() {
    let isValid = true
    errors = {
      firstName: '',
      lastName: '',
      email: '',
      password: '',
      confirmPassword: '',
      acceptTerms: ''
    }

    if (!formData.firstName.trim()) {
      errors.firstName = 'First name is required'
      isValid = false
    }

    if (!formData.lastName.trim()) {
      errors.lastName = 'Last name is required'
      isValid = false
    }

    if (!formData.email.trim()) {
      errors.email = 'Email is required'
      isValid = false
    } else if (!validateEmail(formData.email)) {
      errors.email = 'Please enter a valid email address'
      isValid = false
    }

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

    if (formData.password !== formData.confirmPassword) {
      errors.confirmPassword = 'Passwords do not match'
      isValid = false
    }

    if (!formData.acceptTerms) {
      errors.acceptTerms = 'You must accept the terms and conditions'
      isValid = false
    }

    return isValid
  }

  function handleSubmit() {
    if (validateForm()) {
      console.log('Registration data:', formData)
      // Submit to your API
      alert('Registration successful!')
    }
  }
</script>

<form on:submit|preventDefault={handleSubmit} class="form-container">
  <div class="form-row">
    <TextField
      label="First Name"
      bind:value={formData.firstName}
      error={!!errors.firstName}
      errorText={errors.firstName}
      required
    />

    <TextField
      label="Last Name"
      bind:value={formData.lastName}
      error={!!errors.lastName}
      errorText={errors.lastName}
      required
    />
  </div>

  <TextField
    label="Email"
    type="email"
    bind:value={formData.email}
    error={!!errors.email}
    errorText={errors.email}
    helperText="We'll never share your email"
    required
  />

  <TextField
    label="Password"
    type="password"
    bind:value={formData.password}
    error={!!errors.password}
    errorText={errors.password}
    helperText="Must be at least 8 characters"
    required
  />

  <TextField
    label="Confirm Password"
    type="password"
    bind:value={formData.confirmPassword}
    error={!!errors.confirmPassword}
    errorText={errors.confirmPassword}
    required
  />

  <Checkbox
    label="I accept the terms and conditions"
    bind:checked={formData.acceptTerms}
    error={!!errors.acceptTerms}
    errorText={errors.acceptTerms}
  />

  <div class="button-group">
    <Button type="submit" variant="filled" size="large">
      Register
    </Button>
    <Button type="button" variant="outlined" on:click={() => {
      formData = {
        firstName: '',
        lastName: '',
        email: '',
        password: '',
        confirmPassword: '',
        acceptTerms: false
      }
      errors = {
        firstName: '',
        lastName: '',
        email: '',
        password: '',
        confirmPassword: '',
        acceptTerms: ''
      }
    }}>
      Reset
    </Button>
  </div>
</form>

<style>
  .form-container {
    max-width: 600px;
    margin: 0 auto;
    padding: 2rem;
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
  }

  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }

  .button-group {
    display: flex;
    gap: 1rem;
    justify-content: flex-end;
    margin-top: 1rem;
  }

  @media (max-width: 640px) {
    .form-row {
      grid-template-columns: 1fr;
    }

    .button-group {
      flex-direction: column;
    }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • Complete form validation: Client-side validation with error messages
  • Error state handling: Using Quaff's error props to show validation errors
  • Form reset: Clearing form data and errors
  • Responsive layout: Grid layout that adapts to screen size
  • Material Design 3 styling: Automatic styling from Quaff components

Advanced: Form with Select and Date Picker

For more complex forms, Quaff provides additional components:

<script>
  import { TextField, Button, Select, MenuItem, DatePicker } from 'quaff'

  let formData = {
    name: '',
    country: '',
    birthDate: null,
    preferences: []
  }

  const countries = [
    { value: 'us', label: 'United States' },
    { value: 'uk', label: 'United Kingdom' },
    { value: 'ca', label: 'Canada' },
    { value: 'au', label: 'Australia' }
  ]

  function handleSubmit() {
    console.log('Form data:', formData)
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <TextField
    label="Full Name"
    bind:value={formData.name}
    required
  />

  <Select
    label="Country"
    bind:value={formData.country}
    required
  >
    {#each countries as country}
      <MenuItem value={country.value}>
        {country.label}
      </MenuItem>
    {/each}
  </Select>

  <DatePicker
    label="Date of Birth"
    bind:value={formData.birthDate}
    required
  />

  <Button type="submit" variant="filled">
    Submit
  </Button>
</form>
Enter fullscreen mode Exit fullscreen mode

Common Issues / Troubleshooting

Form values not updating

  • Ensure you're using bind:value (not just value) for two-way binding
  • Check that the variable is declared with let, not const
  • Verify the component name matches Quaff's API (case-sensitive)

Validation errors not showing

  • Make sure you're passing both error={true} and errorText="message" props
  • Check that error state is reactive (use $: for computed error states if needed)
  • Verify the error message is a non-empty string when error is true

Styling conflicts

  • Quaff uses Material Design 3 CSS variables - ensure they're available globally
  • Check for CSS specificity conflicts with your custom styles
  • Use Quaff's built-in variant props instead of overriding styles directly

Components not rendering

  • Verify Quaff is installed: npm list quaff
  • Check import paths match your project structure
  • Ensure you're using the correct component names (check Quaff documentation)
  • For SvelteKit, components should work in both server and client components

Next Steps

Now that you understand how to build forms with Quaff, consider exploring:

  • Form state management: Integrating with stores or form libraries like Felte or Svelte Forms Lib
  • Server-side validation: Connecting client forms to API endpoints with validation
  • Advanced components: Explore Quaff's other components like Data Tables, Navigation Drawers, and Dialogs
  • Theming: Customizing Material Design 3 tokens to match your brand
  • Accessibility: Testing forms with screen readers and keyboard navigation
  • Form libraries integration: Using Quaff components with form validation libraries

For more information, visit the Quaff documentation

Summary

Quaff provides a powerful set of Material Design 3 components that make building forms in Svelte straightforward and accessible. You should now be able to create forms with text inputs, validation, error handling, and various input types. The components handle styling, accessibility, and user interaction patterns automatically, allowing you to focus on your application logic.

Top comments (0)