In modern web applications, forms are a critical component, but managing various types of form inputs with consistent styling and functionality can be challenging. To address this, we’ve developed a dynamic form component in React using TailwindCSS that is flexible, handles a variety of input types, and offers a seamless user experience. This component allows developers to pass a JSON schema, which automatically renders form elements with a consistent UI, making it adaptable for any form requirement.

The Dynamic Form Component

Here’s an overview of the form component we’ve built:

  • Handles Multiple Input Types: The component supports text, number, email, password, radio buttons, checkboxes, and select dropdowns.
  • Automatic Styling: Using TailwindCSS, we’ve ensured that each element has consistent styling, spacing, and a modern, polished look.
  • Customizable Theme Color: Developers can set a themeColor prop to control the primary color throughout the form, affecting elements like buttons, focus rings, and icons.
  • Password Toggle: For password fields, an eye icon appears, allowing users to toggle between hiding and showing the password for convenience.
  • Centered and Spaced Layout: The form is centrally aligned on the page with generous spacing between elements for a clean and modern UI.

Let’s dive into the code to see how this component is implemented:

import React, { useState, useMemo } from 'react';

const FormComponent = ({ fields, themeColor = 'blue' }) => {
  const [formData, setFormData] = useState({});
  const [showPassword, setShowPassword] = useState({});

  const handleChange = (e, name) => {
    setFormData((prevData) => ({
      ...prevData,
      [name]: e.target.value,
    }));
  };

  const togglePasswordVisibility = (name) => {
    setShowPassword((prev) => ({
      ...prev,
      [name]: !prev[name],
    }));
  };

  const themeClasses = useMemo(() => ({
    input:
      `w-full px-4 py-3 border rounded focus:outline-none focus:ring-2 focus:ring-${themeColor}-500`,
    label: 'block text-gray-700 font-semibold mb-2',
    button: `w-full px-4 py-2 bg-${themeColor}-500 text-white rounded hover:bg-${themeColor}-600 focus:outline-none focus:ring-2 focus:ring-${themeColor}-500`,
    checkboxRadio: `form-${themeColor}`,
    icon: `text-${themeColor}-600 cursor-pointer`,
  }), [themeColor]);

  const renderInput = (field) => {
    const { type, label, name, options, placeholder } = field;

    switch (type) {
      case 'text':
      case 'number':
      case 'email':
      case 'date':
        return (
          <div className="mb-6">
            <label className={themeClasses.label}>{label}</label>
            <input
              type={type}
              name={name}
              placeholder={placeholder}
              value={formData[name] || ''}
              onChange={(e) => handleChange(e, name)}
              className={themeClasses.input}
            />
          </div>
        );

      case 'password':
        return (
          <div className="mb-6">
            <label className={themeClasses.label}>{label}</label>
            <div className="relative">
              <input
                type={showPassword[name] ? 'text' : 'password'}
                name={name}
                placeholder={placeholder}
                value={formData[name] || ''}
                onChange={(e) => handleChange(e, name)}
                className={themeClasses.input}
              />
              <span
                onClick={() => togglePasswordVisibility(name)}
                className={`absolute inset-y-0 right-0 flex items-center pr-3 ${themeClasses.icon}`}
              >
                {showPassword[name] ? '👁️' : '👁️‍🗨️'}
              </span>
            </div>
          </div>
        );

      case 'radio':
        return (
          <div className="mb-6">
            <label className={themeClasses.label}>{label}</label>
            <div className="flex items-center space-x-4">
              {options.map((option) => (
                <label key={option.value} className="flex items-center space-x-2">
                  <input
                    type="radio"
                    name={name}
                    value={option.value}
                    checked={formData[name] === option.value}
                    onChange={(e) => handleChange(e, name)}
                    className={themeClasses.checkboxRadio}
                  />
                  <span>{option.label}</span>
                </label>
              ))}
            </div>
          </div>
        );

      case 'checkbox':
        return (
          <div className="mb-6">
            <label className={themeClasses.label}>{label}</label>
            <div className="flex items-center space-x-4">
              {options.map((option) => (
                <label key={option.value} className="flex items-center space-x-2">
                  <input
                    type="checkbox"
                    name={name}
                    value={option.value}
                    checked={formData[name]?.includes(option.value)}
                    onChange={(e) =>
                      setFormData((prevData) => ({
                        ...prevData,
                        [name]: formData[name]
                          ? formData[name].includes(option.value)
                            ? formData[name].filter((val) => val !== option.value)
                            : [...formData[name], option.value]
                          : [option.value],
                      }))
                    }
                    className={themeClasses.checkboxRadio}
                  />
                  <span>{option.label}</span>
                </label>
              ))}
            </div>
          </div>
        );

      case 'select':
        return (
          <div className="mb-6">
            <label className={themeClasses.label}>{label}</label>
            <select
              name={name}
              value={formData[name] || ''}
              onChange={(e) => handleChange(e, name)}
              className={themeClasses.input}
            >
              <option value="">Select {label}</option>
              {options.map((option) => (
                <option key={option.value} value={option.value}>
                  {option.label}
                </option>
              ))}
            </select>
          </div>
        );

      default:
        return null;
    }
  };

  return (
    <div className="flex justify-center items-center min-h-screen bg-gray-100">
      <form className="w-full max-w-lg mx-auto p-8 border rounded-lg shadow-md bg-white">
        <h2 className="text-2xl font-bold text-center mb-6">Dynamic Form</h2>
        {fields.map((field) => (
          <div key={field.name}>{renderInput(field)}</div>
        ))}
        <button type="submit" className={themeClasses.button}>
          Submit
        </button>
      </form>
    </div>
  );
};

export default FormComponent;

How It Works

  1. Customizable UI with TailwindCSS: The form fields, button, and icons use TailwindCSS classes, creating a cohesive design. By passing a themeColor prop (e.g., orange or blue), you can change the color of interactive elements across the entire form.
  2. Responsive and Modern Design: Each field is neatly spaced, and the form is centered on the screen for a balanced layout, making it visually appealing and mobile-friendly.
  3. Dynamic Field Handling: The component dynamically renders input fields based on the passed JSON schema, making it versatile enough to handle different form layouts without additional coding.
  4. Password Visibility Toggle: For password fields, an eye icon appears, allowing users to toggle between showing and hiding their input, a common and user-friendly feature.

Example Usage

To use this form component, pass it a JSON array defining the fields. Here’s an example:

const formFields = [
  { type: 'text', label: 'Name', name: 'name', placeholder: 'Enter your name' },
  { type: 'email', label: 'Email', name: 'email', placeholder: 'Enter your email' },
  { type: 'password', label: 'Password', name: 'password', placeholder: 'Enter your password' },
  { type: 'radio', label: 'Gender', name: 'gender', options: [
      { label: 'Male', value: 'male' },
      { label: 'Female', value: 'female' },
    ]
  },
  { type: 'checkbox', label: 'Interests', name: 'interests', options: [
      { label: 'Music', value: 'music' },
      { label: 'Movies', value: 'movies' },
      { label: 'Sports', value: 'sports' },
    ]
  },
  { type: 'select', label: 'Country', name: 'country', options: [
      { label: 'United States', value: 'us' },
      { label: 'Canada', value: 'ca' },
    ]
  },
];

function App() {
  return (
    <FormComponent fields={formFields} themeColor="orange" />
  );
}

export default App;

Suggestions for Improvement

  1. Validation and Error Handling: Consider adding validation rules, such as required fields and email format checks, with error messages to guide users.
  2. Accessibility Enhancements: Ensure that labels, focus indicators, and keyboard navigation meet accessibility standards, making the form usable for everyone.
  3. Form Submission Handling: Add a submit handler function to process form data after submission, connecting it to a backend service if needed.

By using this dynamic form component, you save time, enhance maintainability, and offer a user-friendly, consistent experience across all forms in your application. Let us know if you have additional suggestions or ideas to make this component even better!