Spam-Protected Contact Form

A contact form with a honeypot, a time check and link blocking to stop spam.

One section file Contact page Edit in the theme editor

Important: Before making any changes to your theme, duplicate it in Online Store > Themes so you have a backup.

Shopify contact forms attract a steady stream of bot spam. This form keeps Shopify's own protection and adds three more layers: the form has no address to post to until JavaScript runs, a hidden honeypot field catches bots that fill in everything, and messages sent too quickly or full of links are stopped.

Preview

What you'll build

Get in touch

Name

Email

Message

Send message Spam protected

Preview with the default settings. Colours, text and layout can all be changed in the theme editor.

Install

Add it to your store in 4 steps

  1. 1

    Open your theme code

    In Shopify admin go to Online Store > Themes. Next to the theme you want to edit, click the three dots and choose Edit code.

  2. 2

    Create the section

    In the Sections folder click Add a new section, name it lt-spam-protected-contact-form, delete the placeholder code, paste in the code below and click Save.

  3. 3

    Add it in the theme editor

    Click Customize, go to Pages > Contact, click Add section and choose Spam-proof contact form.

  4. 4

    Make it yours

    Change the settings in the sidebar, drag the section where you want it, and click Save.

sections/lt-spam-protected-contact-form.liquid · 291 lines
{%- comment -%}
  Spam-protected contact form by LAUNCHTIP
  https://launchtip.com/free-shopify-sections-code
  Three layers on top of Shopify's own spam checks:
  1. The form has no action until JavaScript runs, so simple bots that read
     the HTML and post directly have nowhere to send it.
  2. A hidden honeypot field that people never see but bots fill in.
  3. A minimum time on the page before the form can be sent.
  Optionally, messages containing links are rejected.
{%- endcomment -%}
{%- style -%}
  #lt-{{ section.id }} { padding: {{ section.settings.padding }}px 0; }
  #lt-{{ section.id }} .lt-cf { max-width: 640px; margin: 0 auto; }
  #lt-{{ section.id }} .lt-cf__grid { display: grid; gap: 14px; }
  @media (min-width: 750px) { #lt-{{ section.id }} .lt-cf__grid--two { grid-template-columns: 1fr 1fr; } }
  #lt-{{ section.id }} label { display: block; font-size: 0.9em; font-weight: 600; margin-bottom: 6px; }
  #lt-{{ section.id }} input, #lt-{{ section.id }} textarea, #lt-{{ section.id }} select { width: 100%; padding: 12px; border: 1px solid {{ section.settings.border_color }}; border-radius: 6px; font: inherit; background: #fff; color: #111; }
  #lt-{{ section.id }} textarea { min-height: 140px; resize: vertical; }
  #lt-{{ section.id }} .lt-cf__hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
  #lt-{{ section.id }} .lt-cf__submit { margin-top: 16px; background: {{ section.settings.button_color }}; color: {{ section.settings.button_text }}; border: 0; border-radius: 6px; padding: 13px 24px; font: inherit; font-weight: 600; cursor: pointer; }
  #lt-{{ section.id }} .lt-cf__notice { padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; }
  #lt-{{ section.id }} .lt-cf__notice--ok { background: #f0fdf4; color: #166534; }
  #lt-{{ section.id }} .lt-cf__notice--error { background: #fef2f2; color: #991b1b; }
  #lt-{{ section.id }} .lt-cf__notice--error[hidden] { display: none; }
  #lt-{{ section.id }} .lt-cf__small { font-size: 0.8em; opacity: 0.7; margin-top: 10px; }
{%- endstyle -%}

{%- capture contact_form -%}
  {%- form 'contact', id: 'lt-contact-form' -%}
    {%- if form.posted_successfully? -%}
      <p class="lt-cf__notice lt-cf__notice--ok" role="status">{{ section.settings.success_message }}</p>
    {%- elsif form.errors -%}
      <div class="lt-cf__notice lt-cf__notice--error" role="alert">{{ form.errors | default_errors }}</div>
    {%- endif -%}
    <p class="lt-cf__notice lt-cf__notice--error" role="alert" data-error hidden></p>

    <div class="lt-cf__grid lt-cf__grid--two">
      <div>
        <label for="lt-cf-name-{{ section.id }}">{{ section.settings.name_label }}</label>
        <input id="lt-cf-name-{{ section.id }}" type="text" name="contact[name]" autocomplete="name" value="{{ form.name }}" required>
      </div>
      <div>
        <label for="lt-cf-email-{{ section.id }}">{{ section.settings.email_label }}</label>
        <input id="lt-cf-email-{{ section.id }}" type="email" name="contact[email]" autocomplete="email" autocapitalize="off" spellcheck="false" value="{{ form.email }}" required>
      </div>
    </div>

    <div class="lt-cf__grid" style="margin-top: 14px;">
      {%- if section.settings.show_order_number -%}
        <div>
          <label for="lt-cf-order-{{ section.id }}">{{ section.settings.order_label }}</label>
          <input id="lt-cf-order-{{ section.id }}" type="text" name="contact[Order number]" value="{{ form['Order number'] }}">
        </div>
      {%- endif -%}
      {%- if section.settings.topics != blank -%}
        {%- assign topics = section.settings.topics | split: ',' -%}
        <div>
          <label for="lt-cf-topic-{{ section.id }}">{{ section.settings.topic_label }}</label>
          <select id="lt-cf-topic-{{ section.id }}" name="contact[Topic]">
            {%- for topic in topics -%}
              <option value="{{ topic | strip | escape }}">{{ topic | strip }}</option>
            {%- endfor -%}
          </select>
        </div>
      {%- endif -%}
      <div>
        <label for="lt-cf-body-{{ section.id }}">{{ section.settings.message_label }}</label>
        <textarea id="lt-cf-body-{{ section.id }}" name="contact[body]" required>{{ form.body }}</textarea>
      </div>
    </div>

    <div class="lt-cf__hp" aria-hidden="true">
      <label for="lt-cf-hp-{{ section.id }}">Leave this field empty</label>
      <input id="lt-cf-hp-{{ section.id }}" type="text" name="contact[company_website]" tabindex="-1" autocomplete="off">
    </div>

    <button type="submit" class="lt-cf__submit">{{ section.settings.button_label }}</button>
    {%- if section.settings.small_print != blank -%}
      <p class="lt-cf__small">{{ section.settings.small_print }}</p>
    {%- endif -%}
  {%- endform -%}
{%- endcapture -%}

<div
  id="lt-{{ section.id }}"
  class="page-width"
  data-min-seconds="{{ section.settings.min_seconds }}"
  data-block-links="{{ section.settings.block_links }}"
  data-too-fast="{{ section.settings.too_fast_message | escape }}"
  data-links-message="{{ section.settings.links_message | escape }}"
>
  <div class="lt-cf">
    {%- if section.settings.heading != blank -%}
      <h2 class="h2">{{ section.settings.heading }}</h2>
    {%- endif -%}
    {%- if section.settings.intro != blank -%}
      <div>{{ section.settings.intro }}</div>
    {%- endif -%}
    {{ contact_form | replace: ' action="', ' data-lt-action="' }}
  </div>
</div>

<script>
  (function () {
    var root = document.getElementById('lt-{{ section.id }}');
    if (!root || root.dataset.ready) return;
    root.dataset.ready = '1';

    var form = root.querySelector('form');
    if (!form) return;
    var error = form.querySelector('[data-error]');
    var honeypot = form.querySelector('[name="contact[company_website]"]');
    var body = form.querySelector('[name="contact[body]"]');
    var started = null;

    function start() { if (!started) started = Date.now(); }
    form.addEventListener('focusin', start);
    form.addEventListener('input', start);

    function fail(message) {
      error.textContent = message;
      error.hidden = false;
    }

    form.addEventListener('submit', function (event) {
      var seconds = started ? (Date.now() - started) / 1000 : 0;
      if (honeypot && honeypot.value) {
        event.preventDefault();
        return;
      }
      if (seconds < Number(root.dataset.minSeconds)) {
        event.preventDefault();
        fail(root.dataset.tooFast);
        return;
      }
      if (root.dataset.blockLinks === 'true' && /(https?:\/\/|www\.)/i.test(body.value)) {
        event.preventDefault();
        fail(root.dataset.linksMessage);
        return;
      }
      form.setAttribute('action', form.dataset.ltAction);
    });
  })();
</script>

{% schema %}
{
  "name": "Spam-proof contact form",
  "tag": "section",
  "class": "lt-cf-section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Get in touch"
    },
    {
      "type": "richtext",
      "id": "intro",
      "label": "Intro",
      "default": "<p>We reply within one working day.</p>"
    },
    {
      "type": "text",
      "id": "name_label",
      "label": "Name label",
      "default": "Name"
    },
    {
      "type": "text",
      "id": "email_label",
      "label": "Email label",
      "default": "Email"
    },
    {
      "type": "checkbox",
      "id": "show_order_number",
      "label": "Ask for an order number",
      "default": true
    },
    {
      "type": "text",
      "id": "order_label",
      "label": "Order number label",
      "default": "Order number (optional)"
    },
    {
      "type": "text",
      "id": "topic_label",
      "label": "Topic label",
      "default": "What's it about?"
    },
    {
      "type": "text",
      "id": "topics",
      "label": "Topics",
      "default": "Order status, Returns and exchanges, Product question, Something else",
      "info": "Separated by commas. Leave blank to hide the topic menu."
    },
    {
      "type": "text",
      "id": "message_label",
      "label": "Message label",
      "default": "Message"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Send message"
    },
    {
      "type": "text",
      "id": "success_message",
      "label": "Success message",
      "default": "Thanks, we've got your message and will reply soon."
    },
    {
      "type": "text",
      "id": "small_print",
      "label": "Small print"
    },
    {
      "type": "header",
      "content": "Spam protection"
    },
    {
      "type": "range",
      "id": "min_seconds",
      "label": "Minimum seconds before sending",
      "min": 0,
      "max": 15,
      "step": 1,
      "unit": "s",
      "default": 4
    },
    {
      "type": "text",
      "id": "too_fast_message",
      "label": "Too fast message",
      "default": "That was quick! Please check your message and press send again."
    },
    {
      "type": "checkbox",
      "id": "block_links",
      "label": "Block messages that contain links",
      "default": true
    },
    {
      "type": "text",
      "id": "links_message",
      "label": "Link blocked message",
      "default": "Please remove any links from your message and try again."
    },
    {
      "type": "color",
      "id": "button_color",
      "label": "Button colour",
      "default": "#111111"
    },
    {
      "type": "color",
      "id": "button_text",
      "label": "Button text colour",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "border_color",
      "label": "Field border colour",
      "default": "#d4d4d4"
    },
    {
      "type": "range",
      "id": "padding",
      "label": "Vertical padding",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 40
    }
  ],
  "presets": [
    {
      "name": "Spam-proof contact form"
    }
  ]
}
{% endschema %}
Customization

What you can change in the theme editor

Setting Default
Heading Get in touch
Intro We reply within one working day.
Name label Name
Email label Email
Ask for an order number On
Order number label Order number (optional)
Topic label What's it about?
Topics Separated by commas. Leave blank to hide the topic menu. Order status, Returns and exchanges, Product question, Something else
Message label Message
Button label Send message
Success message Thanks, we've got your message and will reply soon.
Small print —
Minimum seconds before sending 4s
Too fast message That was quick! Please check your message and press send again.
Block messages that contain links On
Link blocked message Please remove any links from your message and try again.
Button colour #111111
Button text colour #ffffff
Field border colour #d4d4d4
Vertical padding 40px
Good to know

Tips for setting it up

  • Keep Shopify's own spam protection on in Online Store > Preferences. This section adds to it rather than replacing it.
  • Remove your theme's existing contact form section from the contact page so there's only one form for bots to find.
  • Leave Block messages that contain links on unless customers often need to send you links.

Further reading: Add a chatbot to your Shopify store

FAQ

Frequently asked questions

Does it work without JavaScript?

No. The form only gets its address when JavaScript runs, which is what stops simple bots. Almost every real shopper has JavaScript on.

Where do the messages go?

To your store's sender email, the same as Shopify's standard contact form.

Can I add more fields?

Yes. Any input named contact[Field name] is included in the email Shopify sends you.

Chatkit

Want this without the code?

Chatkit answers most questions in chat before a shopper reaches the contact form, and only real conversations reach your inbox.

Try Chatkit

More free sections

New

Order Tracking Help Page

A Track your order page with steps, carrier links, common questions and a help box.

New

Size Guide Popup

A size chart popup with a how-to-measure image and a "Not sure which size?" prompt.

Free FAQ Section

Free FAQ Section

Inform customers of store information and policies with this clean FAQ section.

Explore More Free Sections

Discover more code snippets to enhance your Shopify store.

View All Sections
SellUp Increase AOV with Upsells