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

Published November 6th, 2020
3 minute read
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, TailwindCSS, 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

<div x-data="emailSignup()" x-init="trackFormVisit()">
	<div x-cloak x-show="error" x-text="error"></div>
	<div x-cloak x-show="success" x-text="success"></div>
	<form x-ref="signupForm" action="" method="POST">
			<input x-model="email" name="email" placeholder="Your Email Address">
			<button @click.prevent="submit()" :class="{ 'opacity-50': working }" :disabled="working">
				<!-- Loading spinner in the button (optional) -->
				<span x-cloak x-show="working"><svg class="inline-block animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" 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"></path></svg> Working...</span>
				<span x-show="!working">Subscribe</span>

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.

function emailSignup() {
  return {
	// Our "data" -- the email field, messages, and form state
    email: null,
    error: false,
    success: false,
    working: false,

	// When the form is submitted
    submit() {
      // Specify that the form is working (for the loading state)
      this.working = true

      // Using the Fetch API, make a JSON POST request to ConvertKit
      fetch(this.$refs.signupForm.action, {
          method: 'POST',
          headers: {
			Accept: 'application/json',
            'Content-Type': 'application/json',
          body: JSON.stringify({
            api_key: 'YOUR_API_KEY',
      // The response of fetch is a stream object, so two .then()
      // functions are needed here. 
      // See here:
        .then(response => response.json())
        .then(json => {
          // If there's an error, display the message
          if (json.error) {
            this.error = json.message
            this.success = false
          } else {
            // No errors, so we'll display our success message or 
            // a "you've already subscribed" message if that's the case.
            this.error = false
            if (json.subscription.state == 'active') {
              this.success = 'Looks like you are already subscribed, thank you!'
            } else {
              this.success =
                'Success! Please check your email to confirm your subscription.'
        .finally(() => {
          // Hide the loading state
          this.working = false
    // This allows the analytics for the form to work as if it 
    // were a normal form embed from ConvertKit.
    // It is called via x-init in the Alpine component
    trackFormVisit() {
      fetch('', {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',

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.

Join the Newsletter ❤️

A most excellent monthly newsletter with code & design tips, curated links and more!
Don't worry, I'll never send you spam. Unsubscribe at any time.