DEV Community

Lucas Bennett
Lucas Bennett

Posted on

Building Material Design Forms with Smelte in Svelte

Smelte is a UI framework built on top of Svelte and Tailwind CSS that adheres to Material Design specifications, providing a comprehensive set of pre-designed components for building beautiful and responsive web applications. This guide walks through building Material Design forms using Smelte with Svelte, covering everything from basic input fields to form validation and interactive components. This is part 18 of a series on using Smelte with Svelte.

Prerequisites

Before starting, ensure you have:

  • Node.js version 16.x or higher installed
  • SvelteKit project set up (or basic Svelte project with Rollup/Vite)
  • Basic understanding of Svelte components and reactivity
  • Familiarity with Tailwind CSS utility classes (helpful but not required)

Key Concepts to Understand:

  • Material Design components: Pre-styled components following Google's Material Design guidelines that Smelte provides out of the box
  • Form inputs: Text fields, checkboxes, selects, and other input elements with Material Design styling and ripple effects
  • Form validation: Client-side validation using Svelte's reactive statements and bindings to provide user feedback

Installation

Install Smelte using your preferred package manager:

npm install smelte
Enter fullscreen mode Exit fullscreen mode

Or with yarn:

yarn add smelte
Enter fullscreen mode Exit fullscreen mode

Or with pnpm:

pnpm add smelte
Enter fullscreen mode Exit fullscreen mode

This will add Smelte to your package.json dependencies.

Project Setup

1. Configure Rollup Plugin

If you're using Rollup (default for Svelte projects), add the Smelte Rollup plugin to your rollup.config.js. The plugin must be placed after the Svelte plugin but before the CSS plugin:

// rollup.config.js
import svelte from 'rollup-plugin-svelte';
import { smelte } from 'smelte/rollup-plugin-smelte';
import css from 'rollup-plugin-css-only';

const production = !process.env.ROLLUP_WATCH;

export default {
  // ... other config
  plugins: [
    svelte({
      // ... svelte options
    }),
    smelte({
      purge: production,
      output: 'public/global.css', // it defaults to static/global.css
      postcss: [],
      whitelist: [],
      whitelistPatterns: [],
      tailwind: {
        colors: {
          primary: '#b027b0',
          secondary: '#009688',
          error: '#f44336',
          success: '#4caf50',
          alert: '#ff9800',
          blue: '#2196f3',
          dark: '#212121'
        },
        darkMode: 'class',
      },
    }),
    css({ output: 'bundle.css' }),
    // ... other plugins
  ]
};
Enter fullscreen mode Exit fullscreen mode

2. Import Tailwind CSS

In your main application file (typically src/main.js or src/App.svelte), import the Tailwind CSS utilities:

// src/main.js
import App from './App.svelte';
import 'smelte/src/tailwind.css';

const app = new App({
  target: document.body
});

export default app;
Enter fullscreen mode Exit fullscreen mode

3. Include Material Icons and Fonts

Add Material Icons and Roboto font to your HTML template (usually public/index.html or src/app.html):

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons&display=swap" rel="stylesheet" />
  <title>Smelte Forms</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

First Example / Basic Usage

Let's create a simple form with a text field and a button to get started:

<!-- src/components/SimpleForm.svelte -->
<script>
  import Textfield from 'smelte/src/components/Textfield';
  import Button from 'smelte/src/components/Button';

  let name = '';
  let email = '';

  function handleSubmit() {
    console.log('Form submitted:', { name, email });
    alert(`Hello ${name}! Your email is ${email}`);
  }
</script>

<div class="container mx-auto p-8 max-w-md">
  <h1 class="text-2xl font-bold mb-6">Simple Contact Form</h1>

  <form on:submit|preventDefault={handleSubmit}>
    <Textfield
      label="Name"
      bind:value={name}
      class="mb-4"
    />

    <Textfield
      label="Email"
      type="email"
      bind:value={email}
      class="mb-4"
    />

    <Button type="submit" color="primary">
      Submit
    </Button>
  </form>
</div>
Enter fullscreen mode Exit fullscreen mode

What's happening here:

  • Textfield is Smelte's Material Design text input component with built-in styling and animations
  • Button provides Material Design button styling with ripple effects
  • The bind:value directive creates two-way data binding between the inputs and variables
  • The form prevents default submission and handles it with a custom function

Understanding the Basics

Component Import Pattern

Smelte uses tree-shaking, so you should import only the components you need:

<script>
  // Import individual components
  import Button from 'smelte/src/components/Button';
  import Textfield from 'smelte/src/components/Textfield';
  import Checkbox from 'smelte/src/components/Checkbox';
  import Select from 'smelte/src/components/Select';
</script>
Enter fullscreen mode Exit fullscreen mode

Material Design Styling

All Smelte components follow Material Design principles:

  • Elevation: Components have subtle shadows for depth
  • Ripple effects: Interactive elements show ripple animations on click
  • Typography: Uses Roboto font family
  • Colors: Customizable through Tailwind configuration

Two-Way Binding

Smelte components work seamlessly with Svelte's two-way binding:

<script>
  import Textfield from 'smelte/src/components/Textfield';
  let value = '';
</script>

<!-- The value updates reactively -->
<Textfield bind:value={value} />
<p>Current value: {value}</p>
Enter fullscreen mode Exit fullscreen mode

Practical Example / Building Something Real

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

<!-- src/components/RegistrationForm.svelte -->
<script>
  import Textfield from 'smelte/src/components/Textfield';
  import Button from 'smelte/src/components/Button';
  import Checkbox from 'smelte/src/components/Checkbox';
  import Select from 'smelte/src/components/Select';

  // Form data
  let firstName = '';
  let lastName = '';
  let email = '';
  let password = '';
  let confirmPassword = '';
  let country = '';
  let acceptTerms = false;

  // Validation errors
  let errors = {
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: '',
    country: '',
    acceptTerms: ''
  };

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

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

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

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

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

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

    // Confirm password validation
    if (!confirmPassword) {
      errors.confirmPassword = 'Please confirm your password';
      isValid = false;
    } else if (password !== confirmPassword) {
      errors.confirmPassword = 'Passwords do not match';
      isValid = false;
    }

    // Country validation
    if (!country) {
      errors.country = 'Please select a country';
      isValid = false;
    }

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

    return isValid;
  }

  function handleSubmit() {
    if (validateForm()) {
      console.log('Registration data:', {
        firstName,
        lastName,
        email,
        country,
        acceptTerms
      });
      alert('Registration successful!');
      // Reset form
      firstName = '';
      lastName = '';
      email = '';
      password = '';
      confirmPassword = '';
      country = '';
      acceptTerms = false;
    }
  }

  // Country options
  const countries = [
    { value: '', label: 'Select a country' },
    { value: 'us', label: 'United States' },
    { value: 'uk', label: 'United Kingdom' },
    { value: 'ca', label: 'Canada' },
    { value: 'au', label: 'Australia' },
    { value: 'de', label: 'Germany' },
    { value: 'fr', label: 'France' }
  ];
</script>

<div class="container mx-auto p-8 max-w-2xl">
  <h1 class="text-3xl font-bold mb-2">Create Your Account</h1>
  <p class="text-gray-600 mb-8">Fill in the form below to get started</p>

  <form on:submit|preventDefault={handleSubmit} class="space-y-6">
    <!-- Name Fields Row -->
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
      <Textfield
        label="First Name"
        bind:value={firstName}
        error={errors.firstName}
        required
      />

      <Textfield
        label="Last Name"
        bind:value={lastName}
        error={errors.lastName}
        required
      />
    </div>

    <!-- Email Field -->
    <Textfield
      label="Email Address"
      type="email"
      bind:value={email}
      error={errors.email}
      required
      helperText="We'll never share your email with anyone else"
    />

    <!-- Password Fields Row -->
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
      <Textfield
        label="Password"
        type="password"
        bind:value={password}
        error={errors.password}
        required
        helperText="Must be at least 8 characters"
      />

      <Textfield
        label="Confirm Password"
        type="password"
        bind:value={confirmPassword}
        error={errors.confirmPassword}
        required
      />
    </div>

    <!-- Country Select -->
    <Select
      label="Country"
      bind:value={country}
      error={errors.country}
      required
      options={countries}
    />

    <!-- Terms Checkbox -->
    <div class="pt-2">
      <Checkbox
        bind:checked={acceptTerms}
        label="I accept the terms and conditions"
      />
      {#if errors.acceptTerms}
        <p class="text-error text-sm mt-1">{errors.acceptTerms}</p>
      {/if}
    </div>

    <!-- Submit Button -->
    <div class="flex justify-end pt-4">
      <Button type="submit" color="primary" size="large">
        Create Account
      </Button>
    </div>
  </form>
</div>
Enter fullscreen mode Exit fullscreen mode

Key features of this form:

  • Validation: Real-time validation with error messages displayed below each field
  • Multiple input types: Text fields, password fields, select dropdown, and checkbox
  • Responsive layout: Grid layout that adapts to screen size
  • Helper text: Additional guidance for users (e.g., password requirements)
  • Required fields: Visual indicators for mandatory fields
  • Form reset: Clears all fields after successful submission

Common Issues / Troubleshooting

Issue 1: Components not styling correctly

Problem: Smelte components appear unstyled or with default browser styling.

Solution:

  • Ensure you've imported smelte/src/tailwind.css in your main file
  • Verify the Rollup plugin is configured correctly and placed after Svelte plugin
  • Check that Material Icons and Roboto font are loaded in your HTML

Issue 2: Ripple effects not working

Problem: Buttons and interactive elements don't show ripple animations.

Solution:

  • Make sure you're using the correct import path: smelte/src/components/Button
  • Verify that the Smelte Rollup plugin is processing CSS correctly
  • Check browser console for any JavaScript errors

Issue 3: Dark mode not applying

Problem: Dark mode classes aren't working as expected.

Solution:

  • In rollup.config.js, ensure darkMode: 'class' is set in the Tailwind config
  • Add the dark class to your HTML element or a parent container
  • Verify your color palette includes dark mode variants

Issue 4: Form validation not triggering

Problem: Validation errors don't appear when expected.

Solution:

  • Ensure you're using bind:value (not just value) for two-way binding
  • Check that error messages are being set in your validation function
  • Verify the error prop is being passed to the component correctly

Next Steps

Now that you've learned the basics of building forms with Smelte:

  1. Explore more components: Try out Dialogs, Snackbars, and Data Tables for more complex UIs
  2. Customize themes: Modify the color palette in your Rollup config to match your brand
  3. Add animations: Explore Smelte's animation utilities for enhanced user experience
  4. Build complex layouts: Combine forms with navigation, cards, and other Material Design components
  5. Check the documentation: Visit the Smelte GitHub repository for component API references and examples

For more articles in this series, explore other Smelte components and advanced patterns to build complete Material Design applications.

Summary

You've learned how to set up Smelte in a Svelte project and build Material Design forms with validation. You can now create forms with text fields, selects, checkboxes, and buttons, all styled according to Material Design guidelines with built-in ripple effects and animations.

Top comments (0)