React Contact Form with Email (No Backend Required)
Building a contact form in React is straightforward, but sending those form submissions to your email typically requires backend infrastructure. What if you could handle form submissions and receive emails without setting up a Node.js server, configuring SMTP, or managing databases?
In this comprehensive tutorial, you'll learn how to create a production-ready React contact form that sends emails directly to your inbox using Static Forms - no backend code required. We'll use modern React patterns including hooks, implement form validation, add loading states, and include spam protection.
Whether you're building a portfolio, a business website, or any React application that needs a contact form, this guide provides everything you need to implement email functionality in minutes.
What You'll Build
By the end of this tutorial, you'll have a React contact form with:
- ✅ Modern React hooks (useState, useCallback)
- ✅ Form validation with error messages
- ✅ Loading states and user feedback
- ✅ Email notifications to your inbox
- ✅ Spam protection (honeypot + optional reCAPTCHA)
- ✅ Responsive design
- ✅ Accessible markup
Prerequisites
Before starting, make sure you have:
- Basic knowledge of React and JavaScript
- Node.js and npm installed
- A React app created (via Create React App, Vite, or similar)
- A Static Forms account - sign up free
- Your API key from the dashboard
Step 1: Create the Basic Contact Form Component
Let's start by creating a new component called ContactForm.js:
import React, { useState } from 'react';
import './ContactForm.css';
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: '',
});
const [status, setStatus] = useState({
submitting: false,
submitted: false,
error: false,
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setStatus({ submitting: true, submitted: false, error: false });
try {
const response = await fetch('https://api.staticforms.dev/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
apiKey: 'YOUR_API_KEY_HERE', // Replace with your API key
replyTo: formData.email,
}),
});
const result = await response.json();
if (result.success) {
setStatus({ submitting: false, submitted: true, error: false });
setFormData({ name: '', email: '', subject: '', message: '' });
} else {
throw new Error('Form submission failed');
}
} catch (error) {
setStatus({ submitting: false, submitted: false, error: true });
}
};
return (
<div className="contact-form-container">
<h2>Get in Touch</h2>
<p className="subtitle">
Have a question or want to work together? Send us a message!
</p>
{status.submitted && (
<div className="alert alert-success">
✅ Thank you! Your message has been sent successfully. We'll get back to you soon.
</div>
)}
{status.error && (
<div className="alert alert-error">
❌ Oops! Something went wrong. Please try again.
</div>
)}
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-group">
<label htmlFor="name">
Name <span className="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
placeholder="Your name"
/>
</div>
<div className="form-group">
<label htmlFor="email">
Email <span className="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
placeholder="your.email@example.com"
/>
</div>
<div className="form-group">
<label htmlFor="subject">Subject</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
placeholder="What's this about?"
/>
</div>
<div className="form-group">
<label htmlFor="message">
Message <span className="required">*</span>
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
required
rows="5"
placeholder="Tell us what's on your mind..."
/>
</div>
<button
type="submit"
disabled={status.submitting}
className="submit-button"
>
{status.submitting ? 'Sending...' : 'Send Message'}
</button>
</form>
</div>
);
}
export default ContactForm;Step 2: Add Styling
Create ContactForm.css for a professional look:
.contact-form-container {
max-width: 600px;
margin: 0 auto;
padding: 40px 20px;
}
.contact-form-container h2 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 16px;
}
.contact-form {
background: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
font-size: 14px;
}
.required {
color: #e74c3c;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #4A90E2;
}
.form-group textarea {
resize: vertical;
min-height: 120px;
}
.submit-button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.submit-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.submit-button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.alert {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
animation: slideIn 0.3s ease;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.contact-form {
padding: 20px;
}
.contact-form-container h2 {
font-size: 24px;
}
}Step 3: Add Form Validation
Let's enhance the component with client-side validation:
import React, { useState } from 'react';
import './ContactForm.css';
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState({});
const [status, setStatus] = useState({
submitting: false,
submitted: false,
error: false,
});
// Validation function
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
} else if (formData.name.trim().length < 2) {
newErrors.name = 'Name must be at least 2 characters';
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!emailRegex.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
} else if (formData.message.trim().length < 10) {
newErrors.message = 'Message must be at least 10 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error for this field when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
// Validate before submitting
if (!validateForm()) {
return;
}
setStatus({ submitting: true, submitted: false, error: false });
try {
const response = await fetch('https://api.staticforms.dev/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
apiKey: 'YOUR_API_KEY_HERE', // Replace with your API key
replyTo: formData.email,
honeypot: '', // Spam protection
}),
});
const result = await response.json();
if (result.success) {
setStatus({ submitting: false, submitted: true, error: false });
setFormData({ name: '', email: '', subject: '', message: '' });
// Clear success message after 5 seconds
setTimeout(() => {
setStatus(prev => ({ ...prev, submitted: false }));
}, 5000);
} else {
throw new Error('Form submission failed');
}
} catch (error) {
console.error('Submission error:', error);
setStatus({ submitting: false, submitted: false, error: true });
}
};
return (
<div className="contact-form-container">
<h2>Get in Touch</h2>
<p className="subtitle">
Have a question or want to work together? Send us a message!
</p>
{status.submitted && (
<div className="alert alert-success">
✅ Thank you! Your message has been sent successfully. We'll get back to you soon.
</div>
)}
{status.error && (
<div className="alert alert-error">
❌ Oops! Something went wrong. Please try again.
</div>
)}
<form onSubmit={handleSubmit} className="contact-form" noValidate>
<div className="form-group">
<label htmlFor="name">
Name <span className="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
placeholder="Your name"
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="email">
Email <span className="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
placeholder="your.email@example.com"
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="subject">Subject</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
placeholder="What's this about?"
/>
</div>
<div className="form-group">
<label htmlFor="message">
Message <span className="required">*</span>
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
className={errors.message ? 'error' : ''}
rows="5"
placeholder="Tell us what's on your mind..."
/>
{errors.message && <span className="error-message">{errors.message}</span>}
</div>
<button
type="submit"
disabled={status.submitting}
className="submit-button"
>
{status.submitting ? 'Sending...' : 'Send Message'}
</button>
</form>
</div>
);
}
export default ContactForm;Add error styling to your CSS:
/* Add to ContactForm.css */
.form-group input.error,
.form-group textarea.error {
border-color: #e74c3c;
}
.error-message {
display: block;
color: #e74c3c;
font-size: 12px;
margin-top: 5px;
}Step 4: Add reCAPTCHA Protection
For production applications, add spam protection with reCAPTCHA. First, install the React reCAPTCHA package:
npm install react-google-recaptchaUpdate your component:
import React, { useState, useRef } from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
import './ContactForm.css';
function ContactForm() {
const recaptchaRef = useRef();
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState({});
const [recaptchaToken, setRecaptchaToken] = useState('');
const [status, setStatus] = useState({
submitting: false,
submitted: false,
error: false,
});
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!emailRegex.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleRecaptchaChange = (token) => {
setRecaptchaToken(token);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
if (!recaptchaToken) {
setErrors(prev => ({
...prev,
recaptcha: 'Please complete the reCAPTCHA'
}));
return;
}
setStatus({ submitting: true, submitted: false, error: false });
try {
const response = await fetch('https://api.staticforms.dev/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
apiKey: 'YOUR_API_KEY_HERE',
replyTo: formData.email,
'g-recaptcha-response': recaptchaToken,
}),
});
const result = await response.json();
if (result.success) {
setStatus({ submitting: false, submitted: true, error: false });
setFormData({ name: '', email: '', subject: '', message: '' });
setRecaptchaToken('');
if (recaptchaRef.current) {
recaptchaRef.current.reset();
}
} else {
throw new Error('Form submission failed');
}
} catch (error) {
console.error('Submission error:', error);
setStatus({ submitting: false, submitted: false, error: true });
}
};
return (
<div className="contact-form-container">
<h2>Get in Touch</h2>
<p className="subtitle">
Have a question or want to work together? Send us a message!
</p>
{status.submitted && (
<div className="alert alert-success">
✅ Thank you! Your message has been sent successfully.
</div>
)}
{status.error && (
<div className="alert alert-error">
❌ Something went wrong. Please try again.
</div>
)}
<form onSubmit={handleSubmit} className="contact-form" noValidate>
<div className="form-group">
<label htmlFor="name">
Name <span className="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
placeholder="Your name"
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="email">
Email <span className="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
placeholder="your.email@example.com"
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="subject">Subject</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
placeholder="What's this about?"
/>
</div>
<div className="form-group">
<label htmlFor="message">
Message <span className="required">*</span>
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
className={errors.message ? 'error' : ''}
rows="5"
placeholder="Tell us what's on your mind..."
/>
{errors.message && <span className="error-message">{errors.message}</span>}
</div>
<div className="form-group">
<ReCAPTCHA
ref={recaptchaRef}
sitekey="YOUR_RECAPTCHA_SITE_KEY"
onChange={handleRecaptchaChange}
/>
{errors.recaptcha && (
<span className="error-message">{errors.recaptcha}</span>
)}
</div>
<button
type="submit"
disabled={status.submitting}
className="submit-button"
>
{status.submitting ? 'Sending...' : 'Send Message'}
</button>
</form>
</div>
);
}
export default ContactForm;Don't forget to configure your reCAPTCHA secret key in your Static Forms settings.
Learn more in our reCAPTCHA integration guide.
Step 5: Use Environment Variables
For security, store your API keys in environment variables. Create a .env file in your project root:
REACT_APP_STATICFORMS_API_KEY=your_api_key_here
REACT_APP_RECAPTCHA_SITE_KEY=your_recaptcha_site_key_hereUpdate your component to use environment variables:
// In your component
apiKey: process.env.REACT_APP_STATICFORMS_API_KEY,
// For reCAPTCHA
<ReCAPTCHA
sitekey={process.env.REACT_APP_RECAPTCHA_SITE_KEY}
onChange={handleRecaptchaChange}
/>Important: Variables must start with REACT_APP_ to be accessible in Create React App.
Step 6: Add to Your App
Import and use the component in your main app:
// App.js
import React from 'react';
import ContactForm from './components/ContactForm';
import './App.css';
function App() {
return (
<div className="App">
<header>
<h1>My Portfolio</h1>
<nav>
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</nav>
</header>
<main>
<section id="contact">
<ContactForm />
</section>
</main>
<footer>
<p>© 2026 Your Name. All rights reserved.</p>
</footer>
</div>
);
}
export default App;Advanced Features
Custom Hook for Form Handling
Create a reusable custom hook:
// hooks/useContactForm.js
import { useState, useCallback } from 'react';
export const useContactForm = (initialState, apiKey) => {
const [formData, setFormData] = useState(initialState);
const [errors, setErrors] = useState({});
const [status, setStatus] = useState({
submitting: false,
submitted: false,
error: false,
});
const handleChange = useCallback((e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
}, [errors]);
const submitForm = useCallback(async (data) => {
setStatus({ submitting: true, submitted: false, error: false });
try {
const response = await fetch('https://api.staticforms.dev/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...data,
apiKey,
replyTo: data.email,
}),
});
const result = await response.json();
if (result.success) {
setStatus({ submitting: false, submitted: true, error: false });
setFormData(initialState);
return { success: true };
} else {
throw new Error('Submission failed');
}
} catch (error) {
setStatus({ submitting: false, submitted: false, error: true });
return { success: false, error };
}
}, [apiKey, initialState]);
return {
formData,
errors,
status,
setErrors,
handleChange,
submitForm,
};
};Use the custom hook:
import { useContactForm } from './hooks/useContactForm';
function ContactForm() {
const {
formData,
errors,
status,
setErrors,
handleChange,
submitForm,
} = useContactForm(
{ name: '', email: '', subject: '', message: '' },
process.env.REACT_APP_STATICFORMS_API_KEY
);
const handleSubmit = async (e) => {
e.preventDefault();
// Validation logic here
await submitForm(formData);
};
// Rest of the component
}TypeScript Version
For TypeScript projects, add type definitions:
// ContactForm.tsx
import React, { useState, FormEvent, ChangeEvent } from 'react';
interface FormData {
name: string;
email: string;
subject: string;
message: string;
}
interface FormErrors {
name?: string;
email?: string;
message?: string;
recaptcha?: string;
}
interface FormStatus {
submitting: boolean;
submitted: boolean;
error: boolean;
}
const ContactForm: React.FC = () => {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [status, setStatus] = useState<FormStatus>({
submitting: false,
submitted: false,
error: false,
});
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Form submission logic
};
return (
// JSX here
);
};
export default ContactForm;Troubleshooting
Common Issues
Form not submitting:
- Check that your API key is correct
- Verify the API endpoint URL
- Check browser console for errors
- Ensure Content-Type header is set to
application/json
Not receiving emails:
- Verify your API key in the dashboard
- Check your spam folder
- Confirm your email is verified with Static Forms
CORS errors:
- Make sure you're using the correct API endpoint
- Verify you're sending JSON, not FormData
- Check that headers are properly set
Production Checklist
Before deploying to production:
- ✅ Replace
YOUR_API_KEY_HEREwith environment variable - ✅ Add reCAPTCHA for spam protection
- ✅ Test form on different devices and browsers
- ✅ Implement proper error handling
- ✅ Add loading states for better UX
- ✅ Verify email notifications are working
- ✅ Test with various input scenarios
- ✅ Add analytics tracking (optional)
Conclusion
You now have a production-ready React contact form that sends emails without any backend infrastructure. This solution:
- ✅ Works with any React application (CRA, Vite, Next.js, etc.)
- ✅ Provides excellent user experience with validation and feedback
- ✅ Includes spam protection
- ✅ Sends emails reliably to your inbox
- ✅ Requires no server maintenance
Static Forms handles all the complexity of email delivery, spam filtering, and infrastructure management, allowing you to focus on building great React applications.
Ready to add email functionality to your React app? Sign up for Static Forms and start receiving form submissions in minutes!
For framework-specific guides, check out our tutorials for Next.js, Gatsby, and HTML forms.
Related Articles
HTML Form That Sends Email Without Server (2026)
Create an HTML contact form that sends emails directly to your inbox without PHP, Node.js, or any backend code. Complete beginner-friendly guide.
How to Add a Contact Form to Google Sites
Learn how to add a working contact form to your Google Sites website using Static Forms — no backend or coding experience required.
How to Add a Contact Form to Your Astro Website
Step-by-step tutorial for adding a working contact form to Astro sites using Static Forms — no backend required.