🚀 A new era: Kirby 4 Get to know
Skip to content

Contact form

Form handling is always a bit of a pain. In this recipe we will take you step by step through a basic example which you can then extend according to your needs.

This example consists of several parts and files:

  • a contact page
  • a template with the form
  • a controller with the form logic, handling all the "heavy lifting"
  • a plain text and an HTML email snippet

The contact page

Create a contact page with a contact.txt content file. For our means, we only need a title in the content files, the rest is up to you. You could, for example, store an introductory text or some variables – like the recipient email address.

For use in the Panel, you can also create a blueprint for the page. We will skip this step here.

The contact template

Our contact.php template contains the form and will display error messages if something goes wrong or a success message if the form was successfully sent.

<?php snippet('header') ?>
    <main class="main">
        <h1><?= $page->title()->html() ?></h1>

        <?php if($success): ?>
        <div class="alert success">
            <p><?= $success ?></p>
        <?php else: ?>
        <?php if (isset($alert['error'])): ?>
            <div><?= $alert['error'] ?></div>
        <?php endif ?>
        <form method="post" action="<?= $page->url() ?>">
            <div class="honeypot">
                <label for="website">Website <abbr title="required">*</abbr></label>
                <input type="url" id="website" name="website" tabindex="-1">
            <div class="field">
                <label for="name">
                    Name <abbr title="required">*</abbr>
                <input type="text" id="name" name="name" value="<?= esc($data['name'] ?? '', 'attr') ?>" required>
                <?= isset($alert['name']) ? '<span class="alert error">' . esc($alert['name']) . '</span>' : '' ?>
            <div class="field">
                <label for="email">
                    Email <abbr title="required">*</abbr>
                <input type="email" id="email" name="email" value="<?= esc($data['email'] ?? '', 'attr') ?>" required>
                <?= isset($alert['email']) ? '<span class="alert error">' . esc($alert['email']) . '</span>' : '' ?>
            <div class="field">
                <label for="text">
                    Text <abbr title="required">*</abbr>
                <textarea id="text" name="text" required>
                    <?= esc($data['text'] ?? '') ?>
                <?= isset($alert['text']) ? '<span class="alert error">' . esc($alert['text']) . '</span>' : '' ?>
            <input type="submit" name="submit" value="Submit">
        <?php endif ?>

<?php snippet('footer') ?>

The form is displayed by default and hidden once the email was successfully sent. We also included a honeypot field to ensure a minimum level of spam bot protection.

The honeypot field needs to be positioned off the screen, so we need some styles for it. Add this to your stylesheet (you can also change the class and styling, of course).

.honeypot {
    position: absolute;
    left: -9999px;

Because the $data and $alert variables get controlled by user input, it is important to escape the text to protect against XSS vulnerabilities.

The contact form controller

return function($kirby, $pages, $page) {

    $alert = null;

    if($kirby->request()->is('POST') && get('submit')) {

        // check the honeypot
        if(empty(get('website')) === false) {

        $data = [
            'name'  => get('name'),
            'email' => get('email'),
            'text'  => get('text')

        $rules = [
            'name'  => ['required', 'minLength' => 3],
            'email' => ['required', 'email'],
            'text'  => ['required', 'minLength' => 3, 'maxLength' => 3000],

        $messages = [
            'name'  => 'Please enter a valid name',
            'email' => 'Please enter a valid email address',
            'text'  => 'Please enter a text between 3 and 3000 characters'

        // some of the data is invalid
        if($invalid = invalid($data, $rules, $messages)) {
            $alert = $invalid;

            // the data is fine, let's send the email
        } else {
            try {
                    'template' => 'email',
                    'from'     => 'yourcontactform@yourcompany.com',
                    'replyTo'  => $data['email'],
                    'to'       => 'you@yourcompany.com',
                    'subject'  => esc($data['name']) . ' sent you a message from your contact form',
                    'data'     => [
                        'text'   => esc($data['text']),
                        'sender' => esc($data['name'])

            } catch (Exception $error) {
                    $alert['error'] = 'The form could not be sent: <strong>' . $error->getMessage() . '</strong>';
                    $alert['error'] = 'The form could not be sent!';

            // no exception occurred, let's send a success message
            if (empty($alert) === true) {
                $success = 'Your message has been sent, thank you. We will get back to you soon!';
                $data = [];

    return [
        'alert'   => $alert,
        'data'    => $data ?? false,
        'success' => $success ?? false

In our controller, the form evaluation starts once we receive a POST request. First, we check if a bot was stupid enough to get trapped in our honeypot. In this case, we send him back to the page and stop script execution.

Next, we check if all form fields have been filled in according to our validation rules using the invalid() helper:

  • All fields are required and must be filled out.
  • The email field must contain a valid email address.
  • The name field must be at least 3 characters long.
  • The text field must be between 3 and 3000 characters.

You can of course change these rules depending on the type of data you want to obtain and use Kirby's validators or your own custom validators to make sure you get the desired data.

If all went well, we try to send the email in a try - catch block. If that was also successful, we display a success message.

The email templates

In our $kirby->email() method above, we have defined a template we want to use to send the email. In this example, we use a template called email, which is stored in /site/templates/emails.

We can use both, a plain text template and an HTML version. You can read more about this in the email guide.

Here are the two email templates:

The plain text template

The plain text template gets the extension .php.

Hello Company,

<?= $text ?>

<?= $sender ?>

The HTML template

The HTML template gets the extension html.php.

Hello Company,

<p><?= $text ?></p>

<p><?= $sender ?></p>

Both templates are very simple. Kirby provides the variables we defined in the data array ready to be used in the email templates as $text and $sender.

Now you should have a working contact form you can experiment with.

Some ideas for extending this basic example:

  • Progressively enhance with JavaScript validation.
  • Implement some sort of captcha for better spam protection.
  • Integrate other field types.
  • Send files as attachments.
  • Create pages from form data instead of or in addition to sending email.