Every Mother

The Invisible Form Trap: How to Stop Bot Spam Without Making Your Users Solve Puzzles in Statamic

T

As designers, we spend hours obsessing over the perfect form layout. We want it clean, fast, and frictionless. We want the “Submit” button to feel satisfying and the user journey to be seamless.

Then the bot attacks start.

The standard developer advice is almost always: “Just add a CAPTCHA.” But nothing kills a beautiful UI faster than forcing your customers to prove they aren’t a robot by identifying which squares contain a crosswalk or a fire hydrant. It’s clunky, it’s ugly, and it’s a conversion killer.

In my previous post, I showed you how 600+ spam entries actually crashed a client’s dashboard (Statamic 3.1.12). Today, I’m showing you the “invisible” fence I built to make sure that never happens again.

The Concept: What is a Honeypot?

A honeypot is a security trap that exploits the difference between how a human sees a website and how a bot “reads” its code.

  1. Humans don’t see the honeypot field, so they leave it blank.
  2. Bots read the raw HTML code. They see a field labeled fax or phone and their programming forces them to fill it out to bypass what they think is a required field.
  3. The Trap: If the field is filled out, Statamic knows it’s a bot and silently discards the entry.

Step 1: Updating the Blueprint

Before we can add the field to the form, we have to tell Statamic to expect it. If you’re a ‘code first’ developer, editing the YAML file is faster. If you prefer a visual interface, the Control Panel works just as well—Statamic will update the YAML file for you behind the scenes!

YAML – Go into your Form Blueprint and add a simple text field. Pro-tip: Don’t name the handle honeypot. Bots are smart enough to recognize that. Name it something “believable” but obsolete, like fax, middle_name, or secondary_phone. In this example, we’ll use fax.

Before you touch a single line of HTML, you have to tell Statamic that this new field is allowed to exist. If you don’t do this, Statamic will “strip” the field out before it even processes the honeypot logic.

Where to find it: In your project folder, navigate to:

resources/blueprints/forms/your_form_handle.yaml

Open that file and add your honeypot field (we’re using fax) to the sections or fields list. It should look something like this:

YAML

fields:
  -
    handle: fax
    field:
      type: text
      display: Fax

Note: You can also do this in the Control Panel under Tools > Forms > [Your Form] > Blueprint.

Step 2: Coding the Trap (the template)

When you’re building the form in your template, you need to manually add the input. This is where we catch the bots.

Where to find it: Look in resources/views/ (often in page_builder like resources/views/page_builder/_form.antlers.html).

Inside your {{ form:create }} tag, add the trap. If you are using Tailwind CSS, the sr-only class is your best friend. It hides the element visually but keeps it “discoverable” for bots and screen readers. Add the name of your honeypot field to the {form:create}. You may have multiple declarations in that area, for instance I have Alpine.js and Tailwind CSS, but all you need is honeypot="yourname" included.

 {{# Create the selected form with Honeypot enabled. #}}
        {{ form:create in="{ form:handle }"
        id="form"
        class="flex flex-wrap"
        x-ref="form"
        x-data="sending()"
        @submit.prevent="sendForm()"
        honeypot="fax"
        }}

Step 3: The “Loop” Logic

If your template uses a dynamic loop like {{ fields }} ... {{ /fields }}, your new fax field will show up. (If you are using the Peak Starter Kit, you’ll recognize this dynamic field loop.) We need to tell the loop to skip it. Here is the “If” statement logic to keep your UI clean:

{{ fields }}
  {{ if handle !== 'fax' }}
     <div>
        <label>{{ display }}</label>
        {{ field }}
     </div>
  {{ /if }}
{{ /fields }}

If you want to see other ways to use conditional logic in your templates, check out my post on Common IF Statements in Statamic.

If you are using the Peak Starter Kit, you’ll recognize this dynamic field loop.

Step 4: Hiding the Bait (The CSS)

Even though we skipped it in the loop, we still need to manually place the field in the HTML so the bots can find it, but we have to make sure humans never see it.

You can use a class that hides your field, use style:display:none or styling that removes the field off the page’s visible space.

HTML

<div class="sr-only" aria-hidden="true">
  <label for="fax">Fax Number</label>
  <input type="text" id="fax" name="fax" tabindex="-1" autocomplete="off">
</div>

Note: The tabindex="-1" ensures that a human user tabbing through the form with their keyboard will skip over the hidden field entirely, keeping the experience seamless.

Why “sr-only”?

Using Tailwind’s sr-only is much safer than display: none. Some advanced bots are programmed to ignore anything with display: none because they know it’s a trap. sr-only uses a clever combination of clip, width: 1px, and absolute positioning to make the field “invisible” to the eye but very much “real” to a bot’s script.

Putting it together: Here’s what my template code looked like. Review #1 and #2 notes, and adapt for your template.

<template x-if="!success">
    <div class="w-full grid md:grid-cols-12 gap-y-6 md:gap-x-6">

        {{# Step 4. The Trap: Manually placed and hidden from humans #}}
        <div class="sr-only" aria-hidden="true">
            <label for="fax">Fax Number</label>
            <input id="fax" type="text" name="fax" tabindex="-1" autocomplete="off" />
        </div>

        {{# Step 3. The Loop: We tell it to ignore the 'fax' handle so it doesn't render twice #}}
        {{ fields }}
            {{ if handle !== 'fax' }}
                <div class="flex flex-col space-y-4 
                    {{ width == '100' ?= 'md:col-span-12' }} 
                    {{# ... other width logic ... #}}">
                    
                    <label class="font-bold" for="{{ handle }}">
                        {{ trans key="{display}" }}
                        {{ if validate | contains:required }}<sup class="text-red-500">*</sup>{{ /if }}
                    </label>
                    {{ field }}
                </div>
            {{ /if }}
        {{ /fields }}
    </div>
</template>

Step 4: The “Silent Fail”

This is the most satisfying part of the setup. When a bot fills out this form and hits submit, Statamic returns a Success message.

The bot thinks it won. It thinks it successfully sent a spam email. But behind the scenes, Statamic has already identified the fax field was filled, and it simply trashed the entry. No email is sent, and no SPAM file is saved to your storage folder.

By giving the bot a “Success” message, we prevent it from trying a different, more aggressive attack.

Because the honeypot stops the submission at the server level, it prevents your email provider (like Mailgun) from being used to send spam, which protects your sender reputation


The Ethics of Hiding: Making Your Trap ADA Compliant

When we hide a field from a “human,” we have to remember that not all humans navigate the web with their eyes. People using screen readers or keyboard-only navigation can accidentally fall into your trap if you aren’t careful. If a blind user accidentally fills out your “Fax” field because their screen reader announced it, they get blocked just like a bot.

To keep your form accessible and ADA-compliant, we use three specific “stealth” attributes:

1. aria-hidden="true"

This attribute tells screen readers (like VoiceOver or NVDA): “Ignore this entire div and everything inside it.” Without this, a screen reader would announce “Fax Number, edit text,” confusing the user and potentially leading them to fill it out.

2. tabindex="-1"

Sighted users use a mouse, but many users use the Tab key to jump from field to field. By adding tabindex="-1", you ensure that when a user tabs from “Email” to “Message,” the cursor completely skips over the hidden Honeypot field.

3. The sr-only class

As we mentioned, Tailwind’s sr-only is the gold standard. It keeps the field in the “DOM” (the code structure) so bots see it, but it uses CSS to shrink it to a 1×1 pixel size and clip it out of view.

Pro-Tip: Some developers like to add a “Warning” label inside the honeypot just in case. <label for="fax">Fax Number (If you are a human, leave this blank)</label> This is a nice safety net for any assistive technology that might somehow bypass your aria-hidden tag.

UX Wins Every Time

By using a honeypot instead of a CAPTCHA, you are choosing your user’s experience over “standard” security defaults. You get a clean dashboard, your client gets a clean inbox, and your customers get to enjoy the beautiful form you designed without having to solve a puzzle first. And … this is built in, by default, to Statamic sites.

#ven though I’m running an older version of Statamic (v3.1), these core security features have been around since the beginning. It goes to show that a well-built CMS grows with you, rather than becoming obsolete. Helpful Statamic resources:

About the author

Kelly Barkhurst

Designer to Fullstack is my place to geek out and share tech solutions from my day-to-day as a graphic designer, programmer, and business owner (portfolio). I also write on Arts and Bricks, a parenting blog and decal shop that embraces my family’s love of Art and LEGO bricks!

By Kelly Barkhurst May 1, 2026

Recent Posts

Archives

Categories