Last Updated: January 20, 2026
Find us on google
Creating a custom contact form in WordPress doesn’t require expensive plugins. With Bootstrap for styling and PHP for processing, you can build a fully functional, responsive contact form that integrates seamlessly with your site—and you’ll understand exactly how it works.
This guide walks you through building a contact form from scratch, styling it with Bootstrap, handling form submissions, validating input, and sending emails. No bloat. No unnecessary plugins. Just clean, working code.
What You’ll Learn
By the end of this tutorial, you’ll have:
- A fully responsive contact form styled with Bootstrap 5
- Form validation (both frontend and backend)
- Email notifications when users submit
- Security protections (nonces, input sanitization)
- A working example you can customize for any purpose
- Understanding of how the code actually works
Estimated time: 30-45 minutes
Skill level: Beginner to intermediate (some PHP knowledge helpful but not required)
What you need: A WordPress site, access to your theme files or plugin editor
Part 1: Create the HTML Form with Bootstrap
First, we’ll create the HTML structure using Bootstrap 5’s form classes. This gives us a beautiful, mobile-responsive form without writing custom CSS.
Step 1: Create a Custom Page Template
In your WordPress admin:
- Go to Pages → Add New
- Title it “Contact Us” (or whatever you prefer)
- Open the page in your theme editor or create a custom template file
Create a file called page-contact.php in your theme directory (or child theme):
<?php
/**
* Template Name: Contact Form Page
*/
get_header();
?>
<div class="container mt-5 mb-5">
<div class="row">
<div class="col-lg-8 mx-auto">
<h1 class="mb-4">Contact Us</h1>
<?php
// Display success message if form was submitted
if (isset($_GET['form_submitted']) && $_GET['form_submitted'] == 'success') {
echo '<div class="alert alert-success alert-dismissible fade show" role="alert">
<strong>Thank you!</strong> Your message has been sent successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>';
}
?>
<form method="POST" action="<?php echo esc_url(admin_url('admin-ajax.php')); ?>" class="needs-validation" novalidate>
<!-- Nonce field for security -->
<?php wp_nonce_field('contact_form_nonce', 'contact_nonce'); ?>
<!-- Name field -->
<div class="mb-3">
<label for="name" class="form-label">Full Name</label>
<input type="text" class="form-control" id="name" name="name" placeholder="Enter your full name" required>
<div class="invalid-feedback">
Please provide your name.
</div>
</div>
<!-- Email field -->
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="email" name="email" placeholder="your@email.com" required>
<div class="invalid-feedback">
Please provide a valid email address.
</div>
</div>
<!-- Phone field (optional) -->
<div class="mb-3">
<label for="phone" class="form-label">Phone Number (Optional)</label>
<input type="tel" class="form-control" id="phone" name="phone" placeholder="+1 (555) 123-4567">
</div>
<!-- Subject field -->
<div class="mb-3">
<label for="subject" class="form-label">Subject</label>
<input type="text" class="form-control" id="subject" name="subject" placeholder="What is this about?" required>
<div class="invalid-feedback">
Please provide a subject.
</div>
</div>
<!-- Message field -->
<div class="mb-3">
<label for="message" class="form-label">Message</label>
<textarea class="form-control" id="message" name="message" rows="5" placeholder="Type your message here..." required></textarea>
<div class="invalid-feedback">
Please enter a message (at least 10 characters).
</div>
</div>
<!-- Submit button -->
<button type="submit" name="submit_contact_form" class="btn btn-primary btn-lg w-100">
Send Message
</button>
</form>
</div>
</div>
</div>
<script>
// Bootstrap form validation
(function () {
'use strict';
window.addEventListener('load', function () {
var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
}, false);
})();
</script>
<?php get_footer(); ?>
What’s happening:
<form method="POST" action="...">— Sends data to WordPress via AJAXwp_nonce_field()— Security token to prevent unauthorized form submissions- Bootstrap classes like
form-control,form-label,mb-3— Styling and spacing needs-validation— Triggers Bootstrap’s client-side validationrequired— HTML5 validation (checked before PHP validation)
Part 2: Handle Form Processing with PHP
Now we’ll create the PHP script that processes the form, validates the data, and sends emails.
Add this code to your theme’s functions.php file:
<?php
/**
* Handle Contact Form Submission via AJAX
*/
add_action('wp_ajax_nopriv_submit_contact_form', 'handle_contact_form_submission');
add_action('wp_ajax_submit_contact_form', 'handle_contact_form_submission');
function handle_contact_form_submission() {
// Verify nonce for security
if (!isset($_POST['contact_nonce']) || !wp_verify_nonce($_POST['contact_nonce'], 'contact_form_nonce')) {
wp_send_json_error(['message' => 'Security check failed']);
wp_die();
}
// Step 1: Collect and sanitize form data
$name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
$email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
$phone = isset($_POST['phone']) ? sanitize_text_field($_POST['phone']) : '';
$subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : '';
$message = isset($_POST['message']) ? sanitize_textarea_field($_POST['message']) : '';
// Step 2: Validate form data
$errors = [];
// Check required fields
if (empty($name)) {
$errors[] = 'Name is required';
}
if (empty($email)) {
$errors[] = 'Email is required';
} elseif (!is_email($email)) {
$errors[] = 'Please provide a valid email address';
}
if (empty($subject)) {
$errors[] = 'Subject is required';
}
if (empty($message)) {
$errors[] = 'Message is required';
} elseif (strlen($message) < 10) {
$errors[] = 'Message must be at least 10 characters long';
}
// If there are errors, return them
if (!empty($errors)) {
wp_send_json_error(['message' => implode(', ', $errors)]);
wp_die();
}
// Step 3: Prepare email content
$admin_email = get_option('admin_email');
$email_subject = 'New Contact Form Submission: ' . $subject;
$email_body = "You have received a new contact form submission:\n\n";
$email_body .= "Name: " . $name . "\n";
$email_body .= "Email: " . $email . "\n";
if (!empty($phone)) {
$email_body .= "Phone: " . $phone . "\n";
}
$email_body .= "Subject: " . $subject . "\n\n";
$email_body .= "Message:\n" . $message . "\n\n";
$email_body .= "---\n";
$email_body .= "This message was sent from your website: " . get_site_url();
// Step 4: Send email to admin
$headers = [
'From: ' . $name . ' <' . $email . '>',
'Reply-To: ' . $email,
'Content-Type: text/plain; charset=UTF-8'
];
$mail_sent = wp_mail($admin_email, $email_subject, $email_body, $headers);
// Step 5: Send confirmation email to user
$user_subject = 'We received your message';
$user_body = "Hi " . $name . ",\n\n";
$user_body .= "Thank you for contacting us. We received your message and will get back to you as soon as possible.\n\n";
$user_body .= "Best regards,\n";
$user_body .= get_bloginfo('name');
wp_mail($email, $user_subject, $user_body);
// Step 6: Return success response
if ($mail_sent) {
wp_send_json_success(['message' => 'Your message has been sent successfully']);
} else {
wp_send_json_error(['message' => 'Failed to send message. Please try again.']);
}
wp_die();
}
What this does:
- Verifies nonce — Ensures the request came from your form (security)
- Sanitizes input — Removes potentially harmful code from user input
- Validates data — Checks that required fields are filled and valid
- Sends emails — One to admin, one confirmation to user
- Returns response — Tells the form whether it succeeded or failed
Understanding the Security Functions:
sanitize_text_field() // Removes HTML tags, slashes, etc.
sanitize_email() // Ensures valid email format
sanitize_textarea_field() // Allows some HTML but removes dangerous code
is_email() // WordPress function to validate email
wp_verify_nonce() // Confirms the security token is valid
Part 3: Add AJAX Handling (Optional but Recommended)
To make the form submit without page refresh, add this to your page-contact.php template:
<script>
document.addEventListener('DOMContentLoaded', function() {
const contactForm = document.querySelector('form');
if (contactForm) {
contactForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(contactForm);
formData.append('action', 'submit_contact_form');
// Show loading state
const submitBtn = contactForm.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
fetch('<?php echo esc_js(admin_url('admin-ajax.php')); ?>', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
if (data.success) {
// Show success message
alert(data.data.message);
contactForm.reset();
contactForm.classList.remove('was-validated');
} else {
// Show error message
alert('Error: ' + data.data.message);
}
})
.catch(error => {
console.error('Error:', error);
submitBtn.disabled = false;
submitBtn.textContent = originalText;
alert('An error occurred. Please try again.');
});
});
}
});
</script>
This makes the form submit via AJAX (no page refresh) while still working with the PHP handler.
Part 4: Customize the Form for Your Needs
Add More Fields
To add a dropdown or additional field, follow this pattern:
<!-- Select field -->
<div class="mb-3">
<label for="category" class="form-label">Category</label>
<select class="form-select" id="category" name="category" required>
<option value="">Select a category...</option>
<option value="support">Support</option>
<option value="sales">Sales</option>
<option value="partnership">Partnership</option>
</select>
<div class="invalid-feedback">
Please select a category.
</div>
</div>
Then handle it in PHP:
$category = isset($_POST['category']) ? sanitize_text_field($_POST['category']) : '';
Change Email Recipients
Want to send to multiple people? In the PHP handler:
$admin_emails = [
'admin@example.com',
'manager@example.com',
'support@example.com'
];
foreach ($admin_emails as $email) {
wp_mail($email, $email_subject, $email_body, $headers);
}
Customize Email Template
Make the email HTML-formatted:
$headers = [
'From: ' . $name . ' <' . $email . '>',
'Reply-To: ' . $email,
'Content-Type: text/html; charset=UTF-8'
];
$email_body = '<html><body>';
$email_body .= '<h2>New Contact Form Submission</h2>';
$email_body .= '<p><strong>Name:</strong> ' . esc_html($name) . '</p>';
$email_body .= '<p><strong>Email:</strong> ' . esc_html($email) . '</p>';
$email_body .= '<p><strong>Message:</strong></p>';
$email_body .= '<p>' . nl2br(esc_html($message)) . '</p>';
$email_body .= '</body></html>';
Part 5: Test Your Form
- Go to your contact page in the browser
- Try submitting with empty fields — Should show validation errors
- Submit with invalid email — Should show “Invalid email” error
- Submit a valid form — Check your email inbox (might be in spam)
- Check that user receives confirmation email
Common Issues & Fixes
Form doesn’t send:
- Check that emails aren’t going to spam folder
- Verify your site’s email configuration (ask your hosting provider)
- Make sure the page is using the correct template
Validation not working:
- Ensure Bootstrap CSS is loaded on the page
- Check browser console for JavaScript errors
Emails not formatted correctly:
- Use
wp_mail()with proper headers - Test with a simple text email first, then add HTML
Part 6: Add Security & Spam Protection
Optional: Add reCAPTCHA
To prevent spam bots, add Google reCAPTCHA v3:
In your form HTML:
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response">
In your template footer:
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_RECAPTCHA_SITE_KEY"></script>
<script>
grecaptcha.ready(function() {
grecaptcha.execute('YOUR_RECAPTCHA_SITE_KEY', {action: 'submit'}).then(function(token) {
document.getElementById('g-recaptcha-response').value = token;
});
});
</script>
In your PHP handler:
$recaptcha_secret = 'YOUR_RECAPTCHA_SECRET_KEY';
$recaptcha_token = isset($_POST['g-recaptcha-response']) ? $_POST['g-recaptcha-response'] : '';
$verify_url = 'https://www.google.com/recaptcha/api/siteverify';
$response = wp_remote_post($verify_url, [
'body' => [
'secret' => $recaptcha_secret,
'response' => $recaptcha_token
]
]);
$result = json_decode(wp_remote_retrieve_body($response), true);
if ($result['score'] < 0.5) {
wp_send_json_error(['message' => 'Failed security check']);
wp_die();
}
Part 7: Store Submissions in Database (Optional)
Want to save submissions in your WordPress database? Add this to your PHP handler:
// After validation, before sending emails
global $wpdb;
$table_name = $wpdb->prefix . 'contact_submissions';
// Create table (run once)
$wpdb->query("
CREATE TABLE IF NOT EXISTS $table_name (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
phone VARCHAR(20),
subject VARCHAR(200),
message LONGTEXT,
submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(45),
status VARCHAR(20) DEFAULT 'new'
)
");
// Insert submission
$wpdb->insert(
$table_name,
[
'name' => $name,
'email' => $email,
'phone' => $phone,
'subject' => $subject,
'message' => $message,
'ip_address' => $_SERVER['REMOTE_ADDR']
],
['%s', '%s', '%s', '%s', '%s', '%s']
);
Then you can view submissions from WordPress admin by adding a custom admin page (more advanced).
Troubleshooting Checklist
- [ ] Form displays on the page
- [ ] Bootstrap styles are applied (buttons look right, spacing is correct)
- [ ] Form validation shows errors for empty required fields
- [ ] Valid submission goes through without errors
- [ ] Admin email is received
- [ ] User confirmation email is received
- [ ] No fatal PHP errors in debug.log
- [ ] Nonce verification is working
Next Steps
Once your form is working:
- Style it further — Add custom CSS to match your brand
- Create multiple forms — Use different nonces and handlers for different purposes
- Add conditional logic — Show/hide fields based on user selections
- Integrate with CRM — Send submissions to Zapier, Make, or your CRM
- Add file uploads — Allow users to attach documents
The Complete Beginner’s Guide to WordPress Speed Optimization in 2025
Complete Code Reference
All files needed:
page-contact.php— HTML form template with validationfunctions.phpaddition — PHP handler for processing form- Optional AJAX script — For no-refresh submission
Copy, customize, deploy. Your form is ready.

