Static Forms - Secure form backend and form endpoint for contact formsStatic Forms - Secure form backend and form endpoint for contact forms
  • Home
  • Features
  • Docs
  • Blog
  • Pricing
Register
  • Home
  • Features
  • Docs
  • Blog
  • Pricing
Back to all posts

How to Add Document Upload to Your Next.js Contact Form

June 12, 2025
6 min read
Static Forms Team
next.jsreactfile upload
Share:

Building a contact form with document upload functionality can significantly enhance user experience by allowing visitors to share attachments, portfolios, or supporting documents. In this comprehensive guide, we'll walk through creating a robust Next.js contact form with secure file upload capabilities.

What You'll Learn

  • How to implement file upload in React with proper validation
  • Secure file handling with size and type restrictions
  • Error handling for file uploads
  • Form submission with FormData
  • Integration with Static Forms API for seamless form processing

Note: File uploads are available on paid plans (Pro/Advanced, including trials). Free-tier submissions with files will be rejected.

Project Setup

First, let's set up our Next.js project with the necessary dependencies:

Bash
npx create-next-app@latest contact-form-upload --typescript --tailwind --eslint
cd contact-form-upload
npm install

Building the Contact Form Component

Let's create a comprehensive contact form component with document upload functionality:

TypeScript
'use client';

import { useState, useEffect } from 'react';

export default function ContactForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [subject, setSubject] = useState('');
  const [message, setMessage] = useState('');
  const [attachment, setAttachment] = useState<File | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
  const [statusMessage, setStatusMessage] = useState('');

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0] || null;
    
      // File size validation (5MB limit)
  if (file && file.size > 5 * 1024 * 1024) {
    setStatus('error');
    setStatusMessage('File size must be less than 5MB.');
      e.target.value = ''; // Clear the input
      return;
    }
    
    // File type validation
    if (file) {
      const allowedTypes = [
        'image/jpeg', 'image/png', 'image/gif', 'image/webp',
        'application/pdf', 'application/msword', 
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'text/plain', 'application/rtf'
      ];
      
      if (!allowedTypes.includes(file.type)) {
        setStatus('error');
        setStatusMessage('Please upload a valid file type (images, PDF, Word documents, or text files).');
        e.target.value = '';
        return;
      }
    }
    
    setAttachment(file);
    if (status === 'error') {
      setStatus('idle');
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      const formData = new FormData();
      formData.append('apiKey', 'your-static-forms-api-key');
      formData.append('name', name);
      formData.append('email', email);
      formData.append('subject', subject);
      formData.append('message', message);
      formData.append('replyTo', email);
      
      // Add attachment if present
      if (attachment) {
        formData.append('attachment', attachment);
      }
      
      const response = await fetch('https://api.staticforms.dev/submit', {
        method: 'POST',
        body: formData,
      });
      
      if (response.ok) {
        setStatus('success');
        setStatusMessage('Thank you for your message! We will get back to you soon.');
        // Reset form
        setName('');
        setEmail('');
        setSubject('');
        setMessage('');
        setAttachment(null);
        const fileInput = document.getElementById('attachment') as HTMLInputElement;
        if (fileInput) fileInput.value = '';
      } else {
        setStatus('error');
        setStatusMessage('Failed to send your message. Please try again.');
      }
    } catch (error) {
      setStatus('error');
      setStatusMessage('An unexpected error occurred. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h2 className="text-2xl font-bold mb-6">Contact Us</h2>
      
      {status === 'success' && (
        <div className="mb-6 bg-green-50 p-4 rounded-md border border-green-200">
          <p className="text-green-700">{statusMessage}</p>
        </div>
      )}
      
      {status === 'error' && (
        <div className="mb-6 bg-red-50 p-4 rounded-md border border-red-200">
          <p className="text-red-700">{statusMessage}</p>
        </div>
      )}

      <form onSubmit={handleSubmit} className="space-y-6">
        <div>
          <label htmlFor="name" className="block mb-2 font-medium text-gray-700">
            Name <span className="text-red-500">*</span>
          </label>
          <input
            type="text"
            id="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
            required
            className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
          />
        </div>
        
        <div>
          <label htmlFor="email" className="block mb-2 font-medium text-gray-700">
            Email <span className="text-red-500">*</span>
          </label>
          <input
            type="email"
            id="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
            className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
          />
        </div>
        
        <div>
          <label htmlFor="subject" className="block mb-2 font-medium text-gray-700">
            Subject <span className="text-red-500">*</span>
          </label>
          <input
            type="text"
            id="subject"
            value={subject}
            onChange={(e) => setSubject(e.target.value)}
            required
            className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
          />
        </div>
        
        <div>
          <label htmlFor="message" className="block mb-2 font-medium text-gray-700">
            Message <span className="text-red-500">*</span>
          </label>
          <textarea
            id="message"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            required
            rows={5}
            className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
          />
        </div>
        
        <div>
          <label htmlFor="attachment" className="block mb-2 font-medium text-gray-700">
            Attachment <span className="text-gray-500 font-normal">(optional)</span>
          </label>
          <input
            type="file"
            id="attachment"
            onChange={handleFileChange}
            accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.doc,.docx,.txt,.rtf"
            className="w-full p-3 border border-gray-300 rounded-md file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
          />
          <p className="text-sm text-gray-500 mt-1">
            Max file size: 5MB. Accepted formats: Images, PDF, Word documents, text files.
          </p>
          {attachment && (
            <p className="text-sm text-green-600 mt-1">
              Selected: {attachment.name} ({(attachment.size / 1024 / 1024).toFixed(2)} MB)
            </p>
          )}
        </div>
        
        <button
          type="submit"
          disabled={isSubmitting}
          className={`w-full py-3 px-6 rounded-md text-white font-medium transition-colors ${
            isSubmitting 
              ? 'bg-gray-400 cursor-not-allowed' 
              : 'bg-blue-600 hover:bg-blue-700'
          }`}
        >
          {isSubmitting ? 'Sending...' : 'Send Message'}
        </button>
      </form>
    </div>
  );
}

Key Features Explained

File Validation

Our form includes comprehensive file validation to ensure security and user experience:

TypeScript
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0] || null;
  
  // Size validation (5MB limit)
if (file && file.size > 5 * 1024 * 1024) {
  setStatus('error');
  setStatusMessage('File size must be less than 5MB.');
    e.target.value = '';
    return;
  }
  
  // Type validation
  const allowedTypes = [
    'image/jpeg', 'image/png', 'image/gif', 'image/webp',
    'application/pdf', 'application/msword', 
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'text/plain', 'application/rtf'
  ];
  
  if (file && !allowedTypes.includes(file.type)) {
    setStatus('error');
    setStatusMessage('Invalid file type.');
    e.target.value = '';
    return;
  }
};

FormData for File Uploads

When submitting files, we use FormData instead of JSON to properly handle binary data:

TypeScript
const formData = new FormData();
formData.append('apiKey', 'your-api-key');
formData.append('name', name);
formData.append('email', email);
// ... other fields

if (attachment) {
  formData.append('attachment', attachment);
}

Environment Variables

Create a .env.local file to store your API keys securely:

Bash
NEXT_PUBLIC_STATIC_FORMS_API_KEY=your_api_key_here

Then use it in your component:

TypeScript
formData.append('apiKey', process.env.NEXT_PUBLIC_STATIC_FORMS_API_KEY || '');

Setting Up Static Forms

  1. Visit Static Forms and create an account
  2. Create a new form and get your API key
  3. Configure your form settings to accept file uploads
  4. Add your domain to the allowed origins

Enhanced Security Considerations

Client-Side Validation

While we validate files on the client side, remember that client-side validation is primarily for user experience. Always validate files on the server side as well.

File Type Restrictions

Our form accepts common document and image types:

  • Images: JPEG, PNG, GIF, WebP
  • Documents: PDF, Word (.doc, .docx), Plain text, RTF

Size Limitations

We set a 5MB limit to prevent abuse and ensure reasonable upload times:

TypeScript
if (file && file.size > 5 * 1024 * 1024) {
  // Handle oversized file
}

Styling with Tailwind CSS

The form uses Tailwind CSS for responsive, modern styling. Key classes include:

  • file: prefix for styling file input buttons
  • focus:ring-2 for accessibility-friendly focus states
  • transition-colors for smooth hover effects

Advanced Features

Progress Indicator

For large file uploads, consider adding a progress indicator:

TypeScript
const [uploadProgress, setUploadProgress] = useState(0);

// In your fetch request
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
  const progress = (e.loaded / e.total) * 100;
  setUploadProgress(progress);
});

Multiple File Support

To support multiple files, modify the state and input:

TypeScript
const [attachments, setAttachments] = useState<File[]>([]);

// In JSX
<input
  type="file"
  multiple
  onChange={handleMultipleFileChange}
/>

Drag and Drop

Enhance user experience with drag-and-drop functionality:

TypeScript
const handleDrop = (e: React.DragEvent) => {
  e.preventDefault();
  const files = Array.from(e.dataTransfer.files);
  // Process dropped files
};

return (
  <div
    onDrop={handleDrop}
    onDragOver={(e) => e.preventDefault()}
    className="border-2 border-dashed border-gray-300 p-6"
  >
    Drop files here or click to select
  </div>
);

Testing Your Form

  1. Test with different file types and sizes
  2. Verify error messages display correctly
  3. Ensure form resets after successful submission
  4. Test on mobile devices for responsive behavior

Common Troubleshooting

File Not Uploading

  • Check file size limits
  • Verify file type restrictions
  • Ensure FormData is used instead of JSON

Validation Errors

  • Verify all required fields are filled
  • Check file validation logic

Conclusion

Adding document upload functionality to your Next.js contact form enhances user experience and provides valuable functionality for collecting attachments. By implementing proper validation, error handling, and security measures, you can create a robust form that handles file uploads safely and efficiently.

The combination of React's state management, proper file validation, and Static Forms' reliable backend processing creates a seamless experience for both developers and users. Whether you're building a portfolio submission form, support ticket system, or general contact form, these techniques will serve you well.

For more advanced form handling tutorials and Static Forms features, check out our getting started guide and learn about adding contact forms to static sites.

Related Articles

  • Getting Started with Static Forms
  • Adding Contact Forms to Static Sites
  • Using Static Forms with Next.js
Previous

Adding Contact Forms to Static Websites Guide

Next

Implementing Altcha CAPTCHA on HTML Websites

Related Articles

How to Build an Application Form with HTML & JavaScript (2026)

Create a professional application form with file uploads for job applications and program registrations. Complete HTML, React, and Next.js examples with validation and spam protection.

Jan 13, 2026·16 min read

User Registration Form Tutorial: HTML, React & Vue (2026)

Build secure user registration forms with email verification, password validation, and GDPR compliance. Complete examples in HTML, React, and Vue.js.

Jan 13, 2026·16 min read

React Contact Form with Email (No Backend Required)

Build a fully functional React contact form that sends emails without a backend server. Complete tutorial with hooks, validation, and spam protection.

Jan 6, 2026·12 min read
Static Forms - Secure form backend and form endpoint for contact formsStatic Forms - Secure form backend and form endpoint for contact forms

The fastest way to add working contact forms to any website. No backend required.

Product

  • Features
  • Pricing
  • Documentation
  • Changelog

Resources

  • Blog
  • Examples
  • Templates
  • Tools
  • Integrations
  • reCAPTCHA Guide
  • FAQ

Alternatives

  • All Alternatives
  • Formspree
  • Netlify Forms
  • Typeform
  • Formspark

Company

  • Contact
  • About

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DPA

© 2026 Static Forms. All rights reserved.

Committed to sustainability