Vue Contact Form

Without Backend, Sent via Email (or Slack)

Forms Example

This guide will show you how to make a nice-looking and elegant form with Vue. Contact form creation in Vue shouldn't be a tedious task if you're working on a static/jamstack site.

Often, different guides suggest using backend NodeJS/PHP servers for storing/emailing data. But it's not an absolute rule if you think smart, and your codebase can be a lot cleaner with the fronted-only code.

Create the Vue app

(if you're starting a brand new project)

In case you're starting a brand new project, you'll need some initial steps. One of the most straightforward ways is to use the create-vue-app package. To start:

  • Open the terminal and install the create-vue-app package
    npm install @vue/cli --global
  • Then create your app; this will be your project root folder
    vue create vue-project
  • Go to the directory where you will store your project
    cd ~/vue-project
  • When the installation has finished, you can start the server
    npm run serve

Use your favorite code editor to work with files in ~/vue-project/src. You will be able to make a contact form there.

Please keep in mind that the TailwindCSS example is for demo purposes only, for the production please refer to the TailwindCSS installation.

Embed component into your app, enable styling

Open App.vue in your src folder.

1<template>
2  <div class="app-advanced p-10">
3    <img class="mx-auto" alt="Vue logo" src="../assets/logo.png" />
4
5    <form
6      :action="FORM_ENDPOINT"
7      @submit="handleSubmit"
8      method="POST"
9      class="w-1/2 mx-auto mt-5"
10    >
11      <div class="mb-3 pt-0">
12        <input
13          type="text"
14          placeholder="Your name"
15          name="name"
16          class="
17            px-3
18            py-3
19            placeholder-gray-400
20            text-gray-600
21            relative
22            bg-white bg-white
23            rounded
24            text-sm
25            border-0
26            shadow
27            outline-none
28            focus:outline-none
29            focus:ring
30            w-full
31          "
32          required
33        />
34      </div>
35
36      <div class="mb-3 pt-0">
37        <input
38          type="email"
39          placeholder="Email"
40          name="email"
41          class="
42            px-3
43            py-3
44            placeholder-gray-400
45            text-gray-600
46            relative
47            bg-white bg-white
48            rounded
49            text-sm
50            border-0
51            shadow
52            outline-none
53            focus:outline-none
54            focus:ring
55            w-full
56          "
57          required
58        />
59      </div>
60
61      <div class="mb-3 pt-0">
62        <textarea
63          placeholder="Your message"
64          name="message"
65          class="
66            px-3
67            py-3
68            placeholder-gray-400
69            text-gray-600
70            relative
71            bg-white bg-white
72            rounded
73            text-sm
74            border-0
75            shadow
76            outline-none
77            focus:outline-none
78            focus:ring
79            w-full
80          "
81          required
82        />
83      </div>
84
85      <div class="mb-3 pt-0">
86        <button
87          class="
88            bg-blue-500
89            text-white
90            active:bg-blue-600
91            font-bold
92            uppercase
93            text-sm
94            px-6
95            py-3
96            rounded
97            shadow
98            hover:shadow-lg
99            outline-none
100            focus:outline-none
101            mr-1
102            mb-1
103            ease-linear
104            transition-all
105            duration-150
106          "
107          type="submit"
108        >
109          Send a message
110        </button>
111      </div>
112    </form>
113
114    <div v-if="submitted" class="text-center mt-10">
115      <h2 class="text-2xl">Thanks you!</h2>
116      <div class="text-md">We'll be in touch soon.</div>
117    </div>
118  </div>
119</template>
120
121<script>
122export default {
123  name: "App",
124  data: () => ({
125    submitted: false,
126    FORM_ENDPOINT: endpointUrl,
127  }),
128
129  methods: {
130    handleSubmit() {
131      setTimeout(() => {
132        this.submitted = true;
133      }, 100);
134    },
135  },
136
137  // To Add Tailwind
138  beforeCreate() {
139    if (document) {
140      const stylesheet = document.createElement("link");
141      stylesheet.rel = "stylesheet";
142      stylesheet.href =
143        "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css";
144
145      document.head.appendChild(stylesheet);
146    }
147  },
148};
149</script>

Create the free HeroTofu forms backend

Head over to herotofu.com and create an account. It will handle all the boring and complex form submission process work for you. You'll get 14 days of the free trial at first, and later you can leave it with the free forever plan. For the sole purpose of the contact form, it's usually more than enough.

HeroTofu registration is straightforward. Fill in the basic fields and then confirm your email address.

Herotofu signup

Once you have confirmed your email address, go to app.herotofu.com/forms to create your first form. Fill in the form name and add your preferred email address where you'd like to receive your form submits. Slack and Zapier are also options, but you need to pay for them once the trial is over.

You'll get the form endpoint URL once you hit submit, so remember to copy it.

Herotofu Forms List

Use the created forms backend in your contact form

Once again, open the App.vue file and fill in the form endpoint URL. You need to change the FORM_ENDPOINT variable.

Done! Go ahead and test your contact form submission! You don't need to do any backend email work, as HeroTofu will handle everything.

Bonus: advanced Vue implementation

As you can see, the implementation is basic, and you only get those fields on the contact form that are visible on the HTML. That might not work if you need a more customized flow. You'll need to adjust the handleSubmit handler function to inject extra data dynamically. Good examples could be the user id, the selected plan, or some meta-information of site usage. It should make an ajax call to the FORM_ENDPOINT instead of the regular form submits. Here's how it could look like in practice.

1<template>
2  <div class="app-advanced p-10">
3    <img class="mx-auto" alt="Vue logo" src="../assets/logo.png" />
4
5    <form
6      :action="FORM_ENDPOINT"
7      @submit="handleSubmit"
8      method="POST"
9      class="w-1/2 mx-auto mt-5"
10    >
11      <div class="mb-3 pt-0">
12        <input
13          type="text"
14          placeholder="Your name"
15          name="name"
16          class="
17            px-3
18            py-3
19            placeholder-gray-400
20            text-gray-600
21            relative
22            bg-white bg-white
23            rounded
24            text-sm
25            border-0
26            shadow
27            outline-none
28            focus:outline-none
29            focus:ring
30            w-full
31          "
32          required
33        />
34      </div>
35
36      <div class="mb-3 pt-0">
37        <input
38          type="email"
39          placeholder="Email"
40          name="email"
41          class="
42            px-3
43            py-3
44            placeholder-gray-400
45            text-gray-600
46            relative
47            bg-white bg-white
48            rounded
49            text-sm
50            border-0
51            shadow
52            outline-none
53            focus:outline-none
54            focus:ring
55            w-full
56          "
57          required
58        />
59      </div>
60
61      <div class="mb-3 pt-0">
62        <textarea
63          placeholder="Your message"
64          name="message"
65          class="
66            px-3
67            py-3
68            placeholder-gray-400
69            text-gray-600
70            relative
71            bg-white bg-white
72            rounded
73            text-sm
74            border-0
75            shadow
76            outline-none
77            focus:outline-none
78            focus:ring
79            w-full
80          "
81          required
82        />
83      </div>
84
85      <div class="mb-3 pt-0">
86        <button
87          class="
88            bg-blue-500
89            text-white
90            active:bg-blue-600
91            font-bold
92            uppercase
93            text-sm
94            px-6
95            py-3
96            rounded
97            shadow
98            hover:shadow-lg
99            outline-none
100            focus:outline-none
101            mr-1
102            mb-1
103            ease-linear
104            transition-all
105            duration-150
106          "
107          type="submit"
108        >
109          Send a message
110        </button>
111      </div>
112    </form>
113
114    <div v-if="status" class="text-center mt-10">
115      <h2 class="text-2xl">Thanks you!</h2>
116      <div class="text-md">{{ status }}</div>
117    </div>
118  </div>
119</template>
120
121<script>
122import { ref } from "vue";
123export default {
124  setup() {
125    const FORM_ENDPOINT = ref(endpointUrl);
126    const status = ref();
127
128    function handleSubmit(e) {
129      e.preventDefault();
130
131      // Anything you need to inject dynamically
132      const injectedData = {
133        DYNAMIC_DATA_EXAMPLE: 123,
134      };
135
136      const inputs = e.target.elements;
137      const data = {};
138
139      inputs.forEach((inp) => {
140        if (inp.name) {
141          data[inp.name] = inp.value;
142        }
143      });
144
145      Object.assign(data, injectedData);
146
147      fetch(FORM_ENDPOINT.value, {
148        method: "POST",
149        headers: {
150          Accept: "application/json",
151          "Content-Type": "application/json",
152        },
153        body: JSON.stringify(data),
154      })
155        .then((response) => {
156          // It's likely a spam/bot request, so bypass it to validate via captcha
157          if (response.status === 422) {
158            Object.keys(injectedData).forEach((key) => {
159              const el = document.createElement("input");
160              el.type = "hidden";
161              el.name = key;
162              el.value = injectedData[key];
163
164              e.target.appendChild(el);
165            });
166
167            e.target.submit();
168            throw new Error("Please finish the captcha challenge");
169          }
170
171          if (response.status !== 200) {
172            throw new Error(response.statusText);
173          }
174
175          return response.json();
176        })
177        .then(() => (status.value = "We'll be in touch soon."))
178        .catch((err) => (status.value = err.toString()));
179    }
180
181    return { status, handleSubmit, FORM_ENDPOINT };
182  },
183
184  // To Add Tailwind
185  beforeCreate() {
186    if (document) {
187      const stylesheet = document.createElement("link");
188      stylesheet.rel = "stylesheet";
189      stylesheet.href =
190        "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css";
191
192      document.head.appendChild(stylesheet);
193    }
194  },
195};
196</script>

The core idea is that you send a POST request to the FORM_ENDPOINT of your contact form. But it doesn't matter which way you do it. It can be a regular form submit with ajax handler upon form submission or a completely different request that doesn't involve HTML forms at all

Treat it as a REST API because as long as you're sending POST, you'll be good to go. Your vue form submission will reach your inbox.

Have Frontend Only Vue Form?

Notice

We and selected third parties collect personal information as specified in the privacy policy and use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.

Use the “Accept” button to consent. Use the “Reject” button or close this notice to continue without accepting.