Professional Contact Forms in Statamic

David Childs

Master creating a beautiful, functional contact form in Statamic with responsive design, email notifications, and professional validation.

A professional contact form is essential for any business website. In this tutorial, I'll show you how to build a complete contact form in Statamic that not only looks great but also handles submissions reliably with proper email notifications. We'll create exactly what you see on my contact page - a two-column layout with the form on the left and contact information on the right.

What We're Building

We'll create:

  • A responsive contact form with validation
  • Email notifications with HTML and plain text templates
  • A two-column layout with contact information sidebar
  • Social media links integration
  • Success and error message handling
  • Spam protection with honeypot fields

Step 1: Create the Form Configuration

First, we need to define our form structure. Create a new file at resources/forms/contact.yaml:

title: 'Contact Form'
handle: contact
honeypot: winnie
fields:
  -
    handle: name
    field:
      display: Name
      type: text
      required: true
      validate:
        - required
  -
    handle: email
    field:
      display: Email
      type: text
      input_type: email
      required: true
      validate:
        - required
        - email
  -
    handle: subject
    field:
      display: Subject
      type: text
      required: true
      validate:
        - required
  -
    handle: message
    field:
      display: Message
      type: textarea
      required: true
      validate:
        - required
        - min:10
      character_limit: 5000
      rows: 8
  -
    handle: phone
    field:
      display: 'Phone Number (Optional)'
      type: text
      required: false
store: true
email:
  -
    to: '{{ config:mail.to_address }}'
    from: '{{ config:mail.from_address }}'
    reply_to: '{{ email }}'
    subject: 'Contact Form Submission: {{ subject }}'
    html: emails/contact
    text: emails/contact_text

This configuration:

  • Defines all form fields with validation rules
  • Sets up a honeypot field (winnie) for spam protection
  • Configures email notifications to be sent when the form is submitted
  • Stores submissions for later review

Step 2: Create the Contact Page

Create a page entry at content/collections/pages/contact.md:

---
id: contact
blueprint: pages
title: Contact
template: contact
author: 443cb49f-b30a-4e19-8c4a-fc72691de37d
hide_nav: false
seo:
  title: 'Contact - Your Name | Your Title'
  description: 'Get in touch for consulting on systems engineering, infrastructure design, and technology solutions.'
---
Looking to optimize your technology infrastructure or need expert guidance on systems engineering? I'd love to hear from you. Whether you have questions about my work, want to discuss a potential project, or just want to connect, feel free to reach out.

Step 3: Build the Contact Form Template

Now let's create the beautiful two-column layout. Create resources/views/contact.antlers.html:

<!-- Hero Section -->
<section class="bg-white border-b border-neutral-200">
    <div class="container-custom py-16">
        <div class="max-w-3xl">
            <h1 class="text-4xl md:text-5xl font-bold mb-4">{{ title }}</h1>
            <div class="text-xl text-neutral-600">
                {{ content }}
            </div>
        </div>
    </div>
</section>

<!-- Contact Section -->
<section class="bg-white">
    <div class="container-custom py-16">
        <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
            <!-- Contact Form -->
            <div class="lg:col-span-2">
                <div class="card p-8">
                    <h2 class="text-2xl font-bold mb-6">Send Me a Message</h2>
                    
                    {{ form:contact }}
                        {{ if success }}
                            <div class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg mb-6">
                                <p class="font-medium">Thank you for your message!</p>
                                <p class="text-sm mt-1">I'll get back to you as soon as possible.</p>
                            </div>
                        {{ elseif errors }}
                            <div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-6">
                                <p class="font-medium">Please fix the following errors:</p>
                                <ul class="list-disc list-inside text-sm mt-2">
                                    {{ errors }}
                                        <li>{{ value }}</li>
                                    {{ /errors }}
                                </ul>
                            </div>
                        {{ /if }}
                        
                        <div class="space-y-6">
                            <!-- Name Field -->
                            <div>
                                <label for="name" class="block text-sm font-medium text-neutral-700 mb-1">
                                    Name <span class="text-red-500">*</span>
                                </label>
                                <input
                                    type="text"
                                    name="name"
                                    id="name"
                                    value="{{ old:name }}"
                                    class="w-full px-4 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent {{ if error:name }}border-red-500{{ /if }}"
                                    required
                                >
                                {{ if error:name }}
                                    <p class="text-red-500 text-sm mt-1">{{ error:name }}</p>
                                {{ /if }}
                            </div>
                            
                            <!-- Email Field -->
                            <div>
                                <label for="email" class="block text-sm font-medium text-neutral-700 mb-1">
                                    Email <span class="text-red-500">*</span>
                                </label>
                                <input
                                    type="email"
                                    name="email"
                                    id="email"
                                    value="{{ old:email }}"
                                    class="w-full px-4 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent {{ if error:email }}border-red-500{{ /if }}"
                                    required
                                >
                                {{ if error:email }}
                                    <p class="text-red-500 text-sm mt-1">{{ error:email }}</p>
                                {{ /if }}
                            </div>
                            
                            <!-- Phone Field (Optional) -->
                            <div>
                                <label for="phone" class="block text-sm font-medium text-neutral-700 mb-1">
                                    Phone Number <span class="text-neutral-500 text-xs">(Optional)</span>
                                </label>
                                <input
                                    type="tel"
                                    name="phone"
                                    id="phone"
                                    value="{{ old:phone }}"
                                    class="w-full px-4 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
                                >
                            </div>
                            
                            <!-- Subject Field -->
                            <div>
                                <label for="subject" class="block text-sm font-medium text-neutral-700 mb-1">
                                    Subject <span class="text-red-500">*</span>
                                </label>
                                <input
                                    type="text"
                                    name="subject"
                                    id="subject"
                                    value="{{ old:subject }}"
                                    class="w-full px-4 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent {{ if error:subject }}border-red-500{{ /if }}"
                                    required
                                >
                                {{ if error:subject }}
                                    <p class="text-red-500 text-sm mt-1">{{ error:subject }}</p>
                                {{ /if }}
                            </div>
                            
                            <!-- Message Field -->
                            <div>
                                <label for="message" class="block text-sm font-medium text-neutral-700 mb-1">
                                    Message <span class="text-red-500">*</span>
                                </label>
                                <textarea
                                    name="message"
                                    id="message"
                                    rows="6"
                                    class="w-full px-4 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent {{ if error:message }}border-red-500{{ /if }}"
                                    required
                                >{{ old:message }}</textarea>
                                {{ if error:message }}
                                    <p class="text-red-500 text-sm mt-1">{{ error:message }}</p>
                                {{ /if }}
                            </div>
                            
                            <!-- Honeypot Field (Hidden) -->
                            <div class="hidden">
                                <input type="text" name="{{ honeypot }}" tabindex="-1" autocomplete="off">
                            </div>
                            
                            <!-- Submit Button -->
                            <div>
                                <button type="submit" class="btn-primary w-full sm:w-auto">
                                    Send Message
                                </button>
                            </div>
                        </div>
                    {{ /form:contact }}
                </div>
            </div>
            
            <!-- Contact Information Sidebar -->
            <div class="space-y-8">
                <!-- Quick Contact Card -->
                <div class="card p-6">
                    <h3 class="font-bold text-lg mb-4">Get in Touch</h3>
                    <div class="space-y-4">
                        <div class="flex items-start">
                            <svg class="w-5 h-5 text-primary-600 mt-1 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
                            </svg>
                            <div>
                                <p class="font-medium text-neutral-900">Email</p>
                                <a href="mailto:{{ config:mail.to_address }}" class="text-primary-600 hover:text-primary-700">
                                    {{ config:mail.to_address }}
                                </a>
                            </div>
                        </div>
                    </div>
                </div>
                
                <!-- Social Media Links Card -->
                <div class="card p-6">
                    <h3 class="font-bold text-lg mb-4">Connect on Social</h3>
                    <div class="space-y-3">
                        <a href="#" class="flex items-center text-neutral-600 hover:text-primary-600 transition-colors">
                            <svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 24 24">
                                <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
                            </svg>
                            LinkedIn
                        </a>
                        
                        <a href="#" class="flex items-center text-neutral-600 hover:text-primary-600 transition-colors">
                            <svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 24 24">
                                <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
                            </svg>
                            GitHub
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>

Step 4: Create Email Templates

HTML Email Template

Create a beautiful HTML email template at resources/views/emails/contact.antlers.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contact Form Submission</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
        }
        .container {
            background-color: #f9fafb;
            border-radius: 8px;
            padding: 30px;
        }
        h1 {
            color: #1f2937;
            font-size: 24px;
            margin-bottom: 20px;
            border-bottom: 2px solid #e5e7eb;
            padding-bottom: 10px;
        }
        .field {
            margin-bottom: 15px;
        }
        .label {
            font-weight: 600;
            color: #6b7280;
            display: block;
            margin-bottom: 5px;
            font-size: 14px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        .value {
            color: #1f2937;
            background-color: white;
            padding: 10px 12px;
            border-radius: 6px;
            border: 1px solid #e5e7eb;
        }
        .message-content {
            color: #1f2937;
            line-height: 1.6;
        }
        .footer {
            margin-top: 30px;
            padding-top: 20px;
            border-top: 1px solid #e5e7eb;
            font-size: 12px;
            color: #9ca3af;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>New Contact Form Submission</h1>
        
        {{ fields }}
            {{ if value && fieldtype != 'spacer' }}
            <div class="field">
                <span class="label">{{ display }}</span>
                <div class="value">
                    {{ if handle == 'email' }}
                        <a href="mailto:{{ value }}">{{ value }}</a>
                    {{ elseif handle == 'message' }}
                        <div class="message-content">
                            {{ value | nl2br | raw }}
                        </div>
                    {{ else }}
                        {{ value }}
                    {{ /if }}
                </div>
            </div>
            {{ /if }}
        {{ /fields }}
        
        <div class="footer">
            <p>This email was sent from {{ config:app.url }}</p>
            <p>Submitted on {{ now format="F j, Y \a\t g:i A" }}</p>
        </div>
    </div>
</body>
</html>

Plain Text Email Template

Create a plain text version at resources/views/emails/contact_text.antlers.html:

NEW CONTACT FORM SUBMISSION
============================

{{ fields }}
{{ if value && fieldtype != 'spacer' }}
{{ if handle != 'message' }}{{ display }}: {{ value }}
{{ /if }}
{{ /if }}
{{ /fields }}

Message:
--------
{{ fields }}
{{ if handle == 'message' }}{{ value }}{{ /if }}
{{ /fields }}

============================
Sent from {{ config:app.url }}
{{ now format="F j, Y \a\t g:i A" }}

Step 5: Configure Email Settings

Update your .env file with your email configuration:

MAIL_MAILER=smtp
MAIL_HOST=smtp.yourprovider.com
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@yourdomain.com"
MAIL_FROM_NAME="${APP_NAME}"
MAIL_TO_ADDRESS="your@email.com"

And add the custom to_address to config/mail.php:

return [
    // ... existing config
    
    'to_address' => env('MAIL_TO_ADDRESS', 'contact@example.com'),
];

Step 6: Add Styling (Optional)

If you're using Tailwind CSS, the classes in the template should work out of the box. For the card component and buttons, add these styles to your CSS:

.card {
    @apply bg-white rounded-lg shadow-md border border-neutral-200;
}

.btn-primary {
    @apply px-6 py-3 bg-primary-600 text-white font-medium rounded-lg 
           hover:bg-primary-700 transition-colors focus:outline-none 
           focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
}

.container-custom {
    @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
}

Key Features Explained

Form Validation

The form includes both client-side (HTML5) and server-side validation:

  • Required fields are marked with red asterisks
  • Email field validates proper email format
  • Message field requires minimum 10 characters
  • Errors display inline under each field

Spam Protection

The honeypot field (winnie) is hidden from users but visible to bots. If it's filled out, the submission is rejected silently.

User Experience

  • Old values persist on validation errors
  • Success message confirms submission
  • Error messages are clear and specific
  • Form fields have proper focus states

Email Handling

  • HTML emails for rich formatting
  • Plain text fallback for compatibility
  • Automatic line break conversion with nl2br
  • Reply-to address set to submitter's email

Testing Your Form

  1. Test validation: Try submitting with empty fields
  2. Test email delivery: Submit a valid form and check your inbox
  3. Test formatting: Include line breaks in your message
  4. Test spam protection: Try filling the honeypot field (using browser dev tools)

Troubleshooting

Emails not sending?

  • Check your .env mail configuration
  • Verify SMTP credentials
  • Check storage/logs/laravel.log for errors
  • Try setting MAIL_MAILER=log to test locally

Message formatting issues?

  • Ensure you're using nl2br | raw filters
  • Check that the textarea field type is correct
  • Verify email template paths are correct

Conclusion

You now have a professional contact form that:

  • Looks great on all devices
  • Validates user input properly
  • Sends formatted email notifications
  • Protects against spam
  • Provides excellent user experience

This setup gives you a solid foundation that you can customize further with additional fields, styling, or functionality as needed. The two-column layout with the sidebar provides a professional appearance while the form handling ensures reliable message delivery.

Share this article

DC

David Childs

Consulting Systems Engineer with over 10 years of experience building scalable infrastructure and helping organizations optimize their technology stack.

Related Articles