React Contact Form with Email (No Backend Required)

12 min read
Static Forms Team

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:

JSX
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:

CSS
.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:

JSX
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:

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:

Bash
npm install react-google-recaptcha

Update your component:

JSX
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:

Plain Text
REACT_APP_STATICFORMS_API_KEY=your_api_key_here
REACT_APP_RECAPTCHA_SITE_KEY=your_recaptcha_site_key_here

Update your component to use environment variables:

JSX
// 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:

JSX
// 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>&copy; 2026 Your Name. All rights reserved.</p>
      </footer>
    </div>
  );
}

export default App;

Advanced Features

Custom Hook for Form Handling

Create a reusable custom hook:

JSX
// 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:

JSX
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:

TypeScript
// 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_HERE with 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.