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
- Test validation: Try submitting with empty fields
- Test email delivery: Submit a valid form and check your inbox
- Test formatting: Include line breaks in your message
- 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.