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
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>
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
});
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>
This example demonstrates:
-
Input component: AgnosticUI's
Inputwithbind:valuefor two-way binding -
Validation: Custom
validateEmailfunction that checks for required field and email format -
Error display: Using
isInvalidandinvalidTextprops 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}
/>
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>
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>
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
ChoiceInputfor 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
isInvalidprop is set totruewhen there's an error - Check that
invalidTextcontains 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 justvalue) - Check that the variable is declared with
let(notconst) - 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
|preventDefaultmodifier: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)