Building a Dynamic Form Generator with Next.js and TailwindCSS

Wildan Anugrah
4 min readJan 8, 2025

--

Forms are essential in almost every application, but creating them repeatedly for different use cases can be tedious. In this guide, I’ll show you how to build a dynamic form generator with Next.js and TailwindCSS, enabling developers to easily configure and render forms based on a JSON configuration object.

Why a Form Generator?

A form generator allows developers to:

  • Dynamically define fields via configuration.
  • Reuse the same component for different forms.
  • Handle validation and data management seamlessly.

n this example, we’ll build a FormGenerator component that:

  • Supports multiple field types (text, email, textarea, select, and file).
  • Handles form data state automatically.
  • Includes default values for fields.
  • Uses TailwindCSS for styling.

Project Setup

First, create a new Next.js project

npx create-next-app form-generator
cd form-generator

Building the Form Generator

The FormGenerator component takes a config object to define the fields, handles form data state, and renders appropriate inputs based on field types.

FormGenerator Component

Here’s the core of the form generator:

import React, { JSX, useEffect } from 'react';

interface FieldConfig {
type: string;
label: string;
name: string;
placeholder?: string;
required?: boolean;
options?: { value: string; label: string }[];
defaultValue?: string; // Add a defaultValue property
}

interface FormConfig {
fields: FieldConfig[];
}

export const FormGenerator = (
{ config, handleSubmit, children, formData, setFormData }:
{
config: FormConfig,
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void,
children: any,
formData: { [key: string]: any },
setFormData: React.Dispatch<React.SetStateAction<{ [key: string]: any }>>
},
) => {

const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev: any) => ({ ...prev, [name]: value }));
};

useEffect(() => {
const initialData: { [key: string]: any } = {};
config.fields.forEach((field) => {
initialData[field.name] = field.defaultValue || '';
});
setFormData(initialData);
}, [config]);

// Map input types to corresponding components
const fieldRenderers: {
[key: string]: (field: FieldConfig) => JSX.Element;
} = {
text: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<input
type="text"
name={field.name}
id={field.name}
placeholder={field.placeholder}
required={field.required}
value={formData[field.name] || ''}
onChange={handleChange}
className="p-2 border rounded w-full"
/>
</div>
),
email: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<input
type="email"
name={field.name}
id={field.name}
placeholder={field.placeholder}
required={field.required}
value={formData[field.name] || ''}
onChange={handleChange}
className="p-2 border rounded w-full"
/>
</div>
),
file: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<input
type="file"
name={field.name}
id={field.name}
required={field.required}
value={formData[field.name] || ''}
onChange={handleChange}
className="p-2 border rounded w-full"
/>
</div>
),
textarea: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<textarea
name={field.name}
id={field.name}
placeholder={field.placeholder}
required={field.required}
value={formData[field.name] || ''}
onChange={handleChange}
className="p-2 border rounded w-full resize-none"
></textarea>
</div>
),
select: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<select
name={field.name}
id={field.name}
required={field.required}
value={formData[field.name] || field.defaultValue || ''} // Use defaultValue if no value is set
onChange={handleChange}
className="p-2 border rounded w-full bg-white"
>
<option value="">Select an option</option>
{field.options?.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
),
};

return (
<form onSubmit={handleSubmit} className="space-y-4 p-4 border rounded-md">
{config.fields.map((field: any) => ( fieldRenderers[field.type] ? fieldRenderers[field.type](field) : null ))}
{children}
</form>
);
};

Using the Form Generator

import { useState } from "react";
import { FormGenerator } from "./FormGenerator";

const formConfig = {
fields: [
{
type: 'text',
label: 'Name',
name: 'name',
placeholder: 'Enter your name',
required: true,
defaultValue: 'John Doe', // Default value for the text input
},
{
type: 'email',
label: 'Email',
name: 'email',
placeholder: 'Enter your email',
required: true,
defaultValue: 'john.doe@example.com', // Default value for the email input
},
{
type: 'textarea',
label: 'Message',
name: 'message',
placeholder: 'Enter your message',
defaultValue: 'Hello! This is a default message.', // Default value for the textarea
},
{
type: 'select',
label: 'Gender',
name: 'gender',
options: [
{ value: 'male', label: 'Male' },
{ value: 'female', label: 'Female' },
],
required: true,
defaultValue: 'male', // Default selected value for the dropdown
},
{
type: 'file',
label: 'Upload File',
name: 'file',
required: true,
// Default values for file inputs are generally not supported for security reasons
},
],
};

const MainHome = () => {
const [formData, setFormData] = useState<{ [key: string]: any }>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Form Data:', formData);
};
return (
<div className="min-h-screen flex items-center justify-center">
<FormGenerator
config={formConfig}
handleSubmit={handleSubmit}
formData={formData}
setFormData={setFormData}>
<button type="submit" className="p-2 bg-blue-500 text-white rounded w-full">
Submit
</button>
</FormGenerator>
</div>
);
};

export default MainHome;

Features

  • Reusable: Configure forms dynamically with a JSON object.
  • Extendable: Add more field types like radio buttons, checkboxes, or date pickers.
  • TailwindCSS Styling: Customize the look and feel easily.

Wrapping Up

This form generator demonstrates the power of React and Next.js for building flexible components. With TailwindCSS, styling is a breeze, making it easy to create professional-looking UIs.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response