React Form with Contact Form
A contact form is often the first form users encounter on a website, so it sets the tone for perceived quality. Using a green success banner instead of a browser alert keeps the experience smooth and on-brand. The subject dropdown — General, Support, Sales, Partnership — routes the message without requiring any extra explanation from the user.
The challenge
Contact forms seem simple but require solid validation and a clear, non-intrusive success state instead of browser alerts.
- Showing a success message without unmounting the entire form (so users can see what they submitted)
- Validating all four fields with clear per-field error messages without a library
- The subject dropdown needs a default empty state that is prevented from being submitted
- Making the form look complete and professional without excess styling code
Working code example
Here is a complete, self-contained working example you can drop directly into any React project. It uses Tailwind CSS for styling and requires no external dependencies beyond React itself.
import { useState } from 'react';
const SUBJECTS = ['General', 'Support', 'Sales', 'Partnership'];
function validate(f) {
const errs = {};
if (!f.name.trim()) errs.name = 'Name is required';
if (!f.email.trim()) errs.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(f.email)) errs.email = 'Enter a valid email';
if (!f.subject) errs.subject = 'Please select a subject';
if (!f.message.trim()) errs.message = 'Message is required';
else if (f.message.trim().length < 10) errs.message = 'Message must be at least 10 characters';
return errs;
}
export default function ContactForm() {
const [form, setForm] = useState({ name: '', email: '', subject: '', message: '' });
const [errors, setErrors] = useState({});
const [success, setSuccess] = useState(false);
const handle = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
if (errors[e.target.name]) setErrors({ ...errors, [e.target.name]: '' });
};
const handleSubmit = (e) => {
e.preventDefault();
const errs = validate(form);
if (Object.keys(errs).length) { setErrors(errs); return; }
setSuccess(true);
};
const inputCls = (field) =>
`w-full border rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-teal-500 ${errors[field] ? 'border-red-400' : 'border-gray-300'}`;
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-2xl shadow space-y-4">
<h2 className="text-xl font-bold text-gray-800">Contact Us</h2>
{success && (
<div className="bg-green-50 border border-green-200 text-green-700 rounded-xl px-4 py-3 text-sm font-medium">
Message sent! We'll get back to you within 24 hours.
</div>
)}
{!success && (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input name="name" value={form.name} onChange={handle} className={inputCls('name')} />
{errors.name && <p className="text-red-500 text-xs mt-1">{errors.name}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input name="email" type="email" value={form.email} onChange={handle} className={inputCls('email')} />
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Subject</label>
<select name="subject" value={form.subject} onChange={handle} className={`${inputCls('subject')} bg-white`}>
<option value="">Select a subject…</option>
{SUBJECTS.map((s) => <option key={s} value={s}>{s}</option>)}
</select>
{errors.subject && <p className="text-red-500 text-xs mt-1">{errors.subject}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Message</label>
<textarea name="message" value={form.message} onChange={handle} rows={4}
style={{ resize: 'none' }} className={inputCls('message')} />
{errors.message && <p className="text-red-500 text-xs mt-1">{errors.message}</p>}
</div>
<button type="submit"
className="w-full bg-teal-600 text-white font-semibold py-2 rounded-lg hover:bg-teal-700 transition">
Send Message
</button>
</form>
)}
</div>
);
}How ReactForm.co helps
ReactForm's visual builder handles all of the above — contact form configuration, validation rules, state management, and responsive layout — without writing a single line of code. Drag fields onto the canvas, configure their properties in the sidebar, and get production-ready React output. You can publish the form and collect responses instantly, or export the JSX to drop into your own codebase.
Build this form visuallyFrequently asked questions
How do I show a success banner instead of an alert after form submission?
Keep a success boolean in state, set it to true on successful submission, and render the banner conditionally with {success && <Banner />}. Instead of hiding the whole form immediately, you can show the banner above the form or replace just the submit button area. This gives users a visible confirmation without a jarring browser dialog.
How do I clear individual field errors when the user starts correcting them?
In your onChange handler, check if the field currently has an error and clear it: if (errors[e.target.name]) setErrors({ ...errors, [e.target.name]: "" }). This gives immediate positive feedback — the red border disappears as soon as the user begins fixing the issue, without waiting for them to submit again.
How do I actually send the contact form data somewhere?
Replace the setSuccess(true) call with a fetch or axios POST to your backend endpoint: await fetch("/api/contact", { method: "POST", body: JSON.stringify(form), headers: { "Content-Type": "application/json" } }). Handle errors with a try/catch. For serverless options, services like Formspree, Resend, or EmailJS provide ready-made endpoints.
Related topics
Build this form visually — no code needed
ReactForm.co handles contact form fields, validation, conditional logic, and responsive layout automatically. Publish in minutes and collect responses for free.

