How to Build an Email Signup Form with Tailwind CSS, AlpineJS and the ConvertKit API

Published November 6th, 2020
14 minute read
Warning!
This was written over two years ago, so some information might be outdated. Frameworks and best practices change. The web moves fast! You may need to adjust a few things if you follow this article word for word.

Recently I added a signup form for my email list to the site. Using ConvertKit's API, Tailwind CSS, and AlpineJS and the Javascript Fetch API made it easy to whip up. The result is a form that's much more customizable than the embeddable forms provided by ConvertKit. This article breaks down how I did it, and how you can too.

Setting Up the Form

Our first order of business is to set up the form. You'll need to replace YOUR_FORM_ID with the ID of the form (within ConvertKit) that you want to capture emails for. I've left out most styling so it's easier to parse the example, but you can always inspect the code on my site to see which tailwind classes (and transitions) I'm using.

The Signup Form

1<div x-data="emailSignup()" x-init="trackFormVisit()">
2 <div x-cloak x-show="error" x-text="error"></div>
3 <div x-cloak x-show="success" x-text="success"></div>
4 <form
5 x-ref="signupForm"
6 action="https://api.convertkit.com/v3/forms/YOUR_FORM_ID/subscribe"
7 method="POST"
8 >
9 <div>
10 <input x-model="email" name="email" placeholder="Your Email Address" />
11 <button
12 @click.prevent="submit()"
13 :class="{ 'opacity-50': working }"
14 :disabled="working"
15 >
16 <!-- Loading spinner in the button (optional) -->
17 <span x-cloak x-show="working"
18 ><svg
19 class="inline-block animate-spin -ml-1 mr-3 h-5 w-5 text-white"
20 xmlns="http://www.w3.org/2000/svg"
21 fill="none"
22 viewBox="0 0 24 24"
23 >
24 <circle
25 class="opacity-25"
26 cx="12"
27 cy="12"
28 r="10"
29 stroke="currentColor"
30 stroke-width="4"
31 ></circle>
32 <path
33 class="opacity-75"
34 fill="currentColor"
35 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
36 ></path>
37 </svg>
38 Working...</span
39 >
40 <span x-show="!working">Subscribe</span>
41 </button>
42 </div>
43 </form>
44</div>
1<div x-data="emailSignup()" x-init="trackFormVisit()">
2 <div x-cloak x-show="error" x-text="error"></div>
3 <div x-cloak x-show="success" x-text="success"></div>
4 <form
5 x-ref="signupForm"
6 action="https://api.convertkit.com/v3/forms/YOUR_FORM_ID/subscribe"
7 method="POST"
8 >
9 <div>
10 <input x-model="email" name="email" placeholder="Your Email Address" />
11 <button
12 @click.prevent="submit()"
13 :class="{ 'opacity-50': working }"
14 :disabled="working"
15 >
16 <!-- Loading spinner in the button (optional) -->
17 <span x-cloak x-show="working"
18 ><svg
19 class="inline-block animate-spin -ml-1 mr-3 h-5 w-5 text-white"
20 xmlns="http://www.w3.org/2000/svg"
21 fill="none"
22 viewBox="0 0 24 24"
23 >
24 <circle
25 class="opacity-25"
26 cx="12"
27 cy="12"
28 r="10"
29 stroke="currentColor"
30 stroke-width="4"
31 ></circle>
32 <path
33 class="opacity-75"
34 fill="currentColor"
35 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
36 ></path>
37 </svg>
38 Working...</span
39 >
40 <span x-show="!working">Subscribe</span>
41 </button>
42 </div>
43 </form>
44</div>

Wiring It Up

Now that the form is in place, we need to hook it up to the ConvertKit API. At this point we'll need the API key, which you can find in your account section in ConvertKit. Just replace YOUR_API_KEY in the code with the correct key. Although it will work, do not use the api_secret here, because we'll be exposing it in client side (Javascript) code.

Instead of revealing this code piece by piece, the main explanations are in the comments. Take a read through, and I'll summarize again after.

1function emailSignup() {
2 return {
3 // Our "data" -- the email field, messages, and form state
4 email: null,
5 error: false,
6 success: false,
7 working: false,
8 
9 // When the form is submitted
10 submit() {
11 // Specify that the form is working (for the loading state)
12 this.working = true
13 
14 // Using the Fetch API, make a JSON POST request to ConvertKit
15 fetch(this.$refs.signupForm.action, {
16 method: 'POST',
17 headers: {
18 Accept: 'application/json',
19 'Content-Type': 'application/json',
20 },
21 body: JSON.stringify({
22 api_key: 'YOUR_API_KEY',
23 email: this.email,
24 }),
25 })
26 // The response of fetch is a stream object, so two .then()
27 // functions are needed here.
28 // See here: https://developers.google.com/web/updates/2015/03/introduction-to-fetch?hl=en
29 .then((response) => response.json())
30 .then((json) => {
31 // If there's an error, display the message
32 if (json.error) {
33 this.error = json.message
34 this.success = false
35 } else {
36 // No errors, so we'll display our success message or
37 // a "you've already subscribed" message if that's the case.
38 this.error = false
39 if (json.subscription.state == 'active') {
40 this.success = 'Looks like you are already subscribed, thank you!'
41 } else {
42 this.success =
43 'Success! Please check your email to confirm your subscription.'
44 }
45 }
46 })
47 .finally(() => {
48 // Hide the loading state
49 this.working = false
50 })
51 },
52 // This allows the analytics for the form to work as if it
53 // were a normal form embed from ConvertKit.
54 // It is called via x-init in the Alpine component
55 trackFormVisit() {
56 fetch('https://app.convertkit.com/forms/1570815/visit', {
57 method: 'POST',
58 headers: {
59 Accept: 'application/json',
60 'Content-Type': 'application/json',
61 },
62 })
63 },
64 }
65}
1function emailSignup() {
2 return {
3 // Our "data" -- the email field, messages, and form state
4 email: null,
5 error: false,
6 success: false,
7 working: false,
8 
9 // When the form is submitted
10 submit() {
11 // Specify that the form is working (for the loading state)
12 this.working = true
13 
14 // Using the Fetch API, make a JSON POST request to ConvertKit
15 fetch(this.$refs.signupForm.action, {
16 method: 'POST',
17 headers: {
18 Accept: 'application/json',
19 'Content-Type': 'application/json',
20 },
21 body: JSON.stringify({
22 api_key: 'YOUR_API_KEY',
23 email: this.email,
24 }),
25 })
26 // The response of fetch is a stream object, so two .then()
27 // functions are needed here.
28 // See here: https://developers.google.com/web/updates/2015/03/introduction-to-fetch?hl=en
29 .then((response) => response.json())
30 .then((json) => {
31 // If there's an error, display the message
32 if (json.error) {
33 this.error = json.message
34 this.success = false
35 } else {
36 // No errors, so we'll display our success message or
37 // a "you've already subscribed" message if that's the case.
38 this.error = false
39 if (json.subscription.state == 'active') {
40 this.success = 'Looks like you are already subscribed, thank you!'
41 } else {
42 this.success =
43 'Success! Please check your email to confirm your subscription.'
44 }
45 }
46 })
47 .finally(() => {
48 // Hide the loading state
49 this.working = false
50 })
51 },
52 // This allows the analytics for the form to work as if it
53 // were a normal form embed from ConvertKit.
54 // It is called via x-init in the Alpine component
55 trackFormVisit() {
56 fetch('https://app.convertkit.com/forms/1570815/visit', {
57 method: 'POST',
58 headers: {
59 Accept: 'application/json',
60 'Content-Type': 'application/json',
61 },
62 })
63 },
64 }
65}

When the Alpine component initializes (x-init), we track that the signup form has been visited. This makes the form play nice with ConvertKit's built in form analytics. When the submit button is clicked, the submit() function runs. From that point, it's a pretty straightforward AJAX request using the Javascript Fetch API. It shows errors if the ConvertKit API returns them, otherwise it shows a success message. That's all there is to it!

If you want to take it a step further, you can package all of this up in a blade component (or similar templating system) and it becomes a nice, reusable email signup form!

Enjoy this article? Follow me on Twitter for more tips, articles and links.
😢 Awww, nobody has liked or mentioned this on Twitter yet.

Want Updates?

Sign up here if you want to stay in the loop about new articles or products I'm making.
I'll never spam you. Unsubscribe at any time.
Copyright ©2024 Austen Cameron