DEV Community

Cover image for Building Accessible Forms with Validation in AgnosticUI and Svelte
Ethan Walker
Ethan Walker

Posted on

Building Accessible Forms with Validation in AgnosticUI and Svelte

AgnosticUI is a framework-agnostic component library that provides accessible, production-ready UI primitives for React, Vue, Angular, and Svelte. It follows WAI-ARIA guidelines and ensures your forms are accessible out of the box. This article covers building complete forms with validation, error handling, and proper accessibility using AgnosticUI's form components in Svelte. This is part 20 of a series on using AgnosticUI with Svelte.

This guide walks through creating a production-ready contact form with multiple input types, validation, error messages, and proper accessibility features using AgnosticUI components.

Prerequisites

Before starting, ensure you have:

  • A Svelte project (SvelteKit or standalone Svelte 4+)
  • Node.js 18+ and npm/pnpm/yarn
  • Basic understanding of Svelte reactivity and form handling
  • Familiarity with HTML form validation concepts

Installation

Install AgnosticUI for Svelte using your preferred package manager:

npm install agnostic-svelte
# or
pnpm add agnostic-svelte
# or
yarn add agnostic-svelte
Enter fullscreen mode Exit fullscreen mode

The package includes all components and their CSS styles.

Project Setup

After installation, you need to import AgnosticUI's CSS in your application. In SvelteKit, add it to your root layout or app.html:

<!-- src/app.html or src/routes/+layout.svelte -->
<script>
  import 'agnostic-svelte/css/common.min.css';
</script>
Enter fullscreen mode Exit fullscreen mode

For standalone Svelte projects, import it in your main entry file:

// main.js
import 'agnostic-svelte/css/common.min.css';
import App from './App.svelte';

const app = new App({
  target: document.body
});
Enter fullscreen mode Exit fullscreen mode

First Example / Basic Usage

Let's start with a simple form containing a text input with validation:

<!-- src/lib/ContactForm.svelte -->
<script>
  import { Input, Button } from 'agnostic-svelte';
  import 'agnostic-svelte/css/common.min.css';

  let email = '';
  let emailError = '';

  function validateEmail(value) {
    if (!value) {
      return 'Email is required';
    }
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      return 'Please enter a valid email address';
    }
    return '';
  }

  function handleEmailBlur() {
    emailError = validateEmail(email);
  }

  function handleSubmit(event) {
    event.preventDefault();
    emailError = validateEmail(email);

    if (!emailError) {
      console.log('Form submitted:', { email });
      // Handle form submission
    }
  }
</script>

<form on:submit={handleSubmit}>
  <Input
    uniqueId="email-input"
    type="email"
    label="Email Address"
    bind:value={email}
    isInvalid={!!emailError}
    invalidText={emailError}
    helpText="We'll never share your email with anyone else."
    on:blur={handleEmailBlur}
  />

  <Button
    type="submit"
    mode="primary"
    isRounded
    isBlock
    class="mts16"
  >
    Submit
  </Button>
</form>
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • Input component: AgnosticUI's Input with bind:value for two-way binding
  • Validation: Custom validateEmail function that checks for required field and email format
  • Error display: Using isInvalid and invalidText props to show validation errors
  • Help text: Providing additional context with helpText
  • Accessibility: All ARIA attributes are handled automatically by AgnosticUI

Understanding the Basics

AgnosticUI components integrate seamlessly with Svelte's reactivity system. Key concepts:

Two-Way Binding

All form components support Svelte's bind:value directive:

<script>
  import { Input } from 'agnostic-svelte';
  let username = '';
</script>

<Input
  uniqueId="username"
  label="Username"
  bind:value={username}
/>
Enter fullscreen mode Exit fullscreen mode

Validation States

Components accept validation props:

  • isInvalid: Boolean to indicate invalid state
  • invalidText: Error message to display
  • helpText: Helper text shown when field is valid

Form Submission

Handle form submission with standard Svelte event handlers:

<form on:submit|preventDefault={handleSubmit}>
  <!-- form fields -->
</form>
Enter fullscreen mode Exit fullscreen mode

Practical Example / Building Something Real

Let's build a complete contact form with multiple field types, validation, and proper error handling:

<!-- src/lib/ContactForm.svelte -->
<script>
  import { Input, Button, ChoiceInput, ChoiceInputOption } from 'agnostic-svelte';
  import 'agnostic-svelte/css/common.min.css';

  // Form state
  let formData = {
    name: '',
    email: '',
    phone: '',
    subject: '',
    message: '',
    contactMethod: '',
    newsletter: false
  };

  // Validation errors
  let errors = {
    name: '',
    email: '',
    phone: '',
    subject: '',
    message: '',
    contactMethod: ''
  };

  // Validation functions
  function validateName(value) {
    if (!value.trim()) {
      return 'Name is required';
    }
    if (value.trim().length < 2) {
      return 'Name must be at least 2 characters';
    }
    return '';
  }

  function validateEmail(value) {
    if (!value) {
      return 'Email is required';
    }
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      return 'Please enter a valid email address';
    }
    return '';
  }

  function validatePhone(value) {
    if (value && value.trim()) {
      const phoneRegex = /^[\d\s\-\(\)]+$/;
      if (!phoneRegex.test(value) || value.replace(/\D/g, '').length < 10) {
        return 'Please enter a valid phone number';
      }
    }
    return '';
  }

  function validateRequired(value, fieldName) {
    if (!value || !value.trim()) {
      return `${fieldName} is required`;
    }
    return '';
  }

  // Handle field blur events
  function handleNameBlur() {
    errors.name = validateName(formData.name);
  }

  function handleEmailBlur() {
    errors.email = validateEmail(formData.email);
  }

  function handlePhoneBlur() {
    errors.phone = validatePhone(formData.phone);
  }

  function handleSubjectBlur() {
    errors.subject = validateRequired(formData.subject, 'Subject');
  }

  function handleMessageBlur() {
    errors.message = validateRequired(formData.message, 'Message');
  }

  // Validate entire form
  function validateForm() {
    errors.name = validateName(formData.name);
    errors.email = validateEmail(formData.email);
    errors.phone = validatePhone(formData.phone);
    errors.subject = validateRequired(formData.subject, 'Subject');
    errors.message = validateRequired(formData.message, 'Message');
    errors.contactMethod = validateRequired(formData.contactMethod, 'Preferred contact method');

    return !Object.values(errors).some(error => error !== '');
  }

  // Handle form submission
  function handleSubmit(event) {
    event.preventDefault();

    if (validateForm()) {
      console.log('Form submitted:', formData);
      // Here you would typically send data to your API
      alert('Form submitted successfully!');

      // Reset form
      formData = {
        name: '',
        email: '',
        phone: '',
        subject: '',
        message: '',
        contactMethod: '',
        newsletter: false
      };
      errors = {
        name: '',
        email: '',
        phone: '',
        subject: '',
        message: '',
        contactMethod: ''
      };
    } else {
      console.log('Form has validation errors:', errors);
    }
  }
</script>

<div class="container" style="max-width: 600px; margin: 0 auto; padding: 2rem;">
  <h1 class="h3 mbe24">Contact Us</h1>

  <form on:submit={handleSubmit}>
    <!-- Name Input -->
    <div class="mbe16">
      <Input
        uniqueId="name-input"
        type="text"
        label="Full Name"
        bind:value={formData.name}
        isInvalid={!!errors.name}
        invalidText={errors.name}
        helpText="Enter your full name"
        required
        on:blur={handleNameBlur}
      />
    </div>

    <!-- Email Input -->
    <div class="mbe16">
      <Input
        uniqueId="email-input"
        type="email"
        label="Email Address"
        bind:value={formData.email}
        isInvalid={!!errors.email}
        invalidText={errors.email}
        helpText="We'll use this to respond to your inquiry"
        required
        on:blur={handleEmailBlur}
      />
    </div>

    <!-- Phone Input (Optional) -->
    <div class="mbe16">
      <Input
        uniqueId="phone-input"
        type="tel"
        label="Phone Number (Optional)"
        bind:value={formData.phone}
        isInvalid={!!errors.phone}
        invalidText={errors.phone}
        helpText="Include country code if outside US"
        on:blur={handlePhoneBlur}
      />
    </div>

    <!-- Subject Input -->
    <div class="mbe16">
      <Input
        uniqueId="subject-input"
        type="text"
        label="Subject"
        bind:value={formData.subject}
        isInvalid={!!errors.subject}
        invalidText={errors.subject}
        required
        on:blur={handleSubjectBlur}
      />
    </div>

    <!-- Message Textarea -->
    <div class="mbe16">
      <Input
        uniqueId="message-input"
        type="textarea"
        label="Message"
        bind:value={formData.message}
        isInvalid={!!errors.message}
        invalidText={errors.message}
        helpText="Please provide details about your inquiry"
        required
        rows="5"
        on:blur={handleMessageBlur}
      />
    </div>

    <!-- Preferred Contact Method (Radio) -->
    <div class="mbe16">
      <ChoiceInput
        uniqueId="contact-method"
        type="radio"
        legendLabel="Preferred Contact Method"
        bind:value={formData.contactMethod}
        isInvalid={!!errors.contactMethod}
        invalidText={errors.contactMethod}
      >
        <ChoiceInputOption label="Email" value="email" />
        <ChoiceInputOption label="Phone" value="phone" />
        <ChoiceInputOption label="Either" value="either" />
      </ChoiceInput>
    </div>

    <!-- Newsletter Checkbox -->
    <div class="mbe24">
      <ChoiceInput
        uniqueId="newsletter"
        type="checkbox"
        isFieldset={false}
        bind:value={formData.newsletter}
      >
        <ChoiceInputOption
          label="Subscribe to our newsletter for updates and tips"
          value={true}
        />
      </ChoiceInput>
    </div>

    <!-- Submit Button -->
    <Button
      type="submit"
      mode="primary"
      isRounded
      isBlock
      size="large"
    >
      Send Message
    </Button>
  </form>
</div>

<style>
  .container {
    font-family: system-ui, -apple-system, sans-serif;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

This complete form includes:

  • Multiple input types: Text, email, tel, and textarea
  • Real-time validation: Validates on blur and on submit
  • Error messages: Displays specific error messages for each field
  • Radio buttons: Using ChoiceInput for contact method selection
  • Checkbox: Newsletter subscription option
  • Accessibility: All fields are properly labeled and have ARIA attributes
  • Form reset: Clears form after successful submission

Common Issues / Troubleshooting

Validation errors not showing

  • Ensure isInvalid prop is set to true when there's an error
  • Check that invalidText contains the error message string
  • Verify the validation function returns a non-empty string for errors

Two-way binding not working

  • Make sure you're using bind:value (not just value)
  • Check that the variable is declared with let (not const)
  • Verify the component supports binding (all AgnosticUI form components do)

CSS styles not applying

  • Ensure you've imported the CSS: import 'agnostic-svelte/css/common.min.css'
  • Check that the import is in the root layout or main entry file
  • Verify the import path matches your package manager's structure

Form submission not preventing default

  • Add |preventDefault modifier: on:submit|preventDefault={handleSubmit}
  • Or call event.preventDefault() at the start of your handler function

Next Steps

Now that you have a working form with validation, consider:

  • Server-side validation: Implement validation on your backend API
  • Async validation: Add email uniqueness checks or other async validations
  • Form state management: Use Svelte stores for complex multi-step forms
  • Custom validation rules: Create reusable validation utilities
  • Accessibility enhancements: Test with screen readers and keyboard navigation
  • Form libraries integration: Consider integrating with form libraries like Felte or Svelte Forms Lib

For more information, visit the AgnosticUI documentation and explore other components like Dialog, Select, and DatePicker for more complex form scenarios.

Summary

You've learned how to build accessible, validated forms using AgnosticUI components in Svelte. The form includes multiple input types, real-time validation, error handling, and proper accessibility features. You can now create production-ready forms that work seamlessly with Svelte's reactivity system while maintaining full accessibility compliance.

Top comments (0)