DEV Community

Ethan Walker
Ethan Walker

Posted on

Building Forms with Validation in carbon-components-svelte and Svelte

carbon-components-svelte is a Svelte component library that implements IBM's Carbon Design System, providing a collection of accessible and reusable UI components for building consistent interfaces. This article covers building complete forms with validation, error handling, and user feedback using carbon-components-svelte form components in Svelte. This is part 7 of a series on using carbon-components-svelte with Svelte.

This guide walks through creating a production-ready contact form with multiple input types, validation, error messages, and proper event handling using carbon-components-svelte components.

Prerequisites

Before starting, ensure you have:

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

Installation

Install carbon-components-svelte using your preferred package manager:

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

The package includes all components and their styles.

Project Setup

After installation, you need to import Carbon styles into your application. In SvelteKit, add the import to your root layout or app.html:

<!-- src/app.html or src/routes/+layout.svelte -->
<style>
  @import 'carbon-components-svelte/css/white.css';
</style>
Enter fullscreen mode Exit fullscreen mode

For standalone Svelte projects, import in your main file:

<!-- App.svelte -->
<style>
  @import 'carbon-components-svelte/css/white.css';
</style>
Enter fullscreen mode Exit fullscreen mode

Carbon provides several themes: white.css (default), g10.css, g90.css, g100.css. Choose the theme that fits your design.

First Example / Basic Usage

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

<!-- src/lib/ContactForm.svelte -->
<script>
  import { TextInput, Button, Form } from 'carbon-components-svelte';

  let email = '';
  let emailError = '';
  let isInvalid = false;

  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);
    isInvalid = !!emailError;
  }

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

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

<Form on:submit={handleSubmit}>
  <TextInput
    id="email-input"
    type="email"
    labelText="Email Address"
    bind:value={email}
    invalid={isInvalid}
    invalidText={emailError}
    helperText="We'll never share your email with anyone else"
    placeholder="Enter your email"
    on:blur={handleEmailBlur}
  />

  <Button type="submit" kind="primary">
    Submit
  </Button>
</Form>
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • TextInput component: TextInput from carbon-components-svelte with bind:value for two-way binding
  • Validation: validateEmail function checks for required field and email format
  • Error display: Using invalid and invalidText props to show validation errors
  • Helper text: Providing additional context with helperText
  • Accessibility: All ARIA attributes are handled automatically by Carbon components

Understanding the Basics

carbon-components-svelte components integrate seamlessly with Svelte's reactivity system. Key concepts:

Two-Way Binding

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

<script>
  import { TextInput } from 'carbon-components-svelte';
  let username = '';
</script>

<TextInput
  id="username"
  labelText="Username"
  bind:value={username}
/>
Enter fullscreen mode Exit fullscreen mode

Validation States

Components accept validation props:

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

Form Submission Handling

Handle form submission with standard Svelte event handlers:

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

Reactive Statements

Use Svelte reactive statements for automatic validation:

<script>
  let password = '';
  let invalid = false;

  // Automatically updates when password changes
  $: invalid = !/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/.test(password);
</script>
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 { 
    Form, 
    TextInput, 
    TextArea, 
    Button,
    FormGroup,
    RadioButtonGroup,
    RadioButton,
    Checkbox
  } from 'carbon-components-svelte';

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

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

  // Field validity state
  let invalidFields = {
    name: false,
    email: false,
    phone: false,
    subject: false,
    message: false,
    contactMethod: false
  };

  // 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);
    invalidFields.name = !!errors.name;
  }

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

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

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

  function handleMessageBlur() {
    errors.message = validateRequired(formData.message, 'Message');
    invalidFields.message = !!errors.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');

    invalidFields.name = !!errors.name;
    invalidFields.email = !!errors.email;
    invalidFields.phone = !!errors.phone;
    invalidFields.subject = !!errors.subject;
    invalidFields.message = !!errors.message;
    invalidFields.contactMethod = !!errors.contactMethod;

    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: ''
      };
      invalidFields = {
        name: false,
        email: false,
        phone: false,
        subject: false,
        message: false,
        contactMethod: false
      };
    } else {
      console.log('Form has validation errors:', errors);
    }
  }
</script>

<div class="form-container">
  <h2>Contact Us</h2>

  <Form on:submit={handleSubmit}>
    <FormGroup legendText="Contact Information">
      <!-- Name field -->
      <TextInput
        id="name-input"
        type="text"
        labelText="Full Name"
        bind:value={formData.name}
        invalid={invalidFields.name}
        invalidText={errors.name}
        helperText="Enter your full name"
        placeholder="John Doe"
        required
        on:blur={handleNameBlur}
      />

      <!-- Email field -->
      <TextInput
        id="email-input"
        type="email"
        labelText="Email Address"
        bind:value={formData.email}
        invalid={invalidFields.email}
        invalidText={errors.email}
        helperText="We'll use this to respond to your inquiry"
        placeholder="john@example.com"
        required
        on:blur={handleEmailBlur}
      />

      <!-- Phone field (optional) -->
      <TextInput
        id="phone-input"
        type="tel"
        labelText="Phone Number (Optional)"
        bind:value={formData.phone}
        invalid={invalidFields.phone}
        invalidText={errors.phone}
        helperText="Include country code if outside US"
        placeholder="+1 (555) 123-4567"
        on:blur={handlePhoneBlur}
      />
    </FormGroup>

    <FormGroup legendText="Message">
      <!-- Subject field -->
      <TextInput
        id="subject-input"
        type="text"
        labelText="Subject"
        bind:value={formData.subject}
        invalid={invalidFields.subject}
        invalidText={errors.subject}
        placeholder="What is this regarding?"
        required
        on:blur={handleSubjectBlur}
      />

      <!-- Message field -->
      <TextArea
        id="message-input"
        labelText="Message"
        bind:value={formData.message}
        invalid={invalidFields.message}
        invalidText={errors.message}
        helperText="Please provide details about your inquiry"
        placeholder="Enter your message..."
        rows="5"
        required
        on:blur={handleMessageBlur}
      />
    </FormGroup>

    <FormGroup legendText="Additional">
      <!-- Preferred contact method (Radio) -->
      <RadioButtonGroup
        id="contact-method"
        labelText="Preferred Contact Method"
        bind:selected={formData.contactMethod}
        invalid={invalidFields.contactMethod}
        invalidText={errors.contactMethod}
      >
        <RadioButton labelText="Email" value="email" id="contact-email" />
        <RadioButton labelText="Phone" value="phone" id="contact-phone" />
        <RadioButton labelText="Either" value="either" id="contact-either" />
      </RadioButtonGroup>

      <!-- Newsletter checkbox -->
      <Checkbox
        id="newsletter"
        labelText="Subscribe to our newsletter for updates and tips"
        bind:checked={formData.newsletter}
      />
    </FormGroup>

    <!-- Submit button -->
    <Button type="submit" kind="primary" size="default">
      Send Message
    </Button>
  </Form>
</div>

<style>
  .form-container {
    max-width: 600px;
    margin: 2rem auto;
    padding: 2rem;
  }

  h2 {
    margin-bottom: 2rem;
    font-size: 1.5rem;
    font-weight: 600;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

This complete form includes:

  • Multiple input types: Text fields, email, phone, and textarea
  • Real-time validation: Validates on blur and on submit
  • Error messages: Displays specific error messages for each field
  • Radio buttons: Using RadioButtonGroup 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 the invalid 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 carbon-components-svelte form components do)

CSS styles not applying

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

Form submission not preventing default behavior

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

Components not rendering correctly

  • Ensure you're using the correct component names (e.g., TextInput, not Input)
  • Check that all necessary components are imported from carbon-components-svelte
  • Verify the library version is compatible with your Svelte version

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 carbon-components-svelte documentation and explore other components like Modal, Select, and DatePicker for more complex form scenarios.

Summary

You've learned how to build accessible, validated forms using carbon-components-svelte 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)