I've previously tried a few different form services — TypeForm, Jotform, Contact Forms 7 and a few others — but I've quickly hit their limits. They're all beautiful and easy to use, and so for a quick website, or one built by people who don't want to write code, they're a good option.

But I don't like them. They're slow, rely on external connections, hard to customise, bad for privacy and security, and ultimately, not much better than writing my own forms. So I decided to write my own. And here's how you can!

If you're thinking of using Ghost to host your website, the main reason I prefer Ghost is the writing experience in Ghost (especially compared to WordPress). The only competitor in writing experience is medium, and Ghost is a far better option than Medium in most cases. Give Ghost a try — you get a 2 week trial.

Why I prefer HTML forms to external form hosts like Typeform, Jotform, etc.

Apps like Typeform and Jotform solve the problem of easily making beautiful forms. They can host your forms, or you can embed your forms in a website.

They're really useful at the early, hacky stage. But the problems with external form hosts like Typeform and Jotform is that

  • They're slow to load, relying on JavaScript just to render sometimes. They add a second or more to your load time. In a world where we try to get load times under two seconds, that's far too much.
  • They load external JavaScript and CSS files, increasing the number of external requests, lowering speed further, and reducing my knowledge of how my website is built. I like to be able to point at any bit of code created and say: I know what that's doing. (Well, as much as possible, anyway...)
  • Customising them to look nice often means writing CSS and HTML anyway, so I start to inch closer to writing my own forms.
  • The data often gets captured in their database, which isn't necessarily secure. I was using TypeForm when it was hacked and it caused a brief crisis (tens of thousands of emails exposed)
  • I need to rely on their APIs and glue connecting forms to things like Google Sheets
  • I have to rely on their interface to design forms, and which is sometimes impractical (e.g. the ConvertKit one wasn't available behind my VPN, which killed it for me... I work often from places like the Middle East or China, where the government spies on people)
  • They cost money, usually $10 a month (which is silly since I pay less to host my whole website)

That's a lot of problems, especially considering the last one — that I'm paying for them all.

So I've started to write my own forms in HTML. I mean, how hard can it be?

The alternative: Writing forms in HTML, and connecting it with Zapier

Forms are one of the built in things that HTML was always supposed to be able to do, since several versions ago of HTML.

When I write my own forms, they're quick to load (basically as quick as any text in a static site). Plus I get full control over design, I get to send the data anywhere I want, and it's free (basically, aside from hosting, and some integration costs).

It does mean learning a little CSS, and plugging in a bit of JavaScript to send the form information anywhere.

It also means having somewhere to receive the form information. You can email it to yourself, or receive it with anything with an API (including a Google Sheets with an API front-end). But a smarter thing is to ingest it with something like Zapier using their Webhooks functionality.

Setting up the HTML form

A form consists of three parts

  • HTML to make the form
  • CSS to make it pretty
  • JS to process and send it
  • Zapier to capture it

Let's start with the HTML. At its core, a form can be as simple as:

<h4>Contact me</h4>
<form method="post">
  <input type="text" name="name" placeholder="Your name">
  <input type="text" name="name" placeholder="your@email.com">
  <textarea rows="5" placeholder="Some message"></textarea>
  <input type="submit" value="Submit">

But that's ugly. So ugly, I don't even want it to be a live form, I'm just going to include a picture!

Pure HTML form in Ghost - ugly version
Ugly pure HTML form in Ghost

Also, the "Submit" button doesn't do anything.

So those are the two things we have to do: 1. Make the form look better, and 2. Make it do something.

Styling the form with CSS

First up, let's make the form look better. Here's our goal.

Attractive forms in Ghost - a much better lookingcontact form
A much better looking form

If you want to style your form differently, my advice is to google around for CSS for forms, and find something you like. Then, borrow! That sounds obvious, but just explaining my layman's process.

To make my form's style, I'm going to create a style block (to avoid creating a separate file). The elements I'm going to modify are: form, input, select, textarea, and button. These make up the whole form.

The main modifications I do in this block are

  • Change the font. I don't know what font you'll use, but I wanted to use a sans-serif font in the form.
  • Adjust the placement. I want it to be full-width, vertically stacked, and with a few margins. This way, it'll look good (and consistent) on different display sizes.
  • Give each field a "selected" behaviour. It's nice to draw attention to whatever field is being modified.
  • Colour the button, and make it fade in/out. I've given the button a behaviour so it changes colour slightly when you hover on it.
  • Catch robots: I've also got a hidden field in there. If a robot fills out the form, it'll never get sent!

All this is to make it look neat (and cool)!

	form,input,select,textarea,button {
        font-family: avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;
		display: block;
        margin: .5rem;
        padding: .375rem .75rem;
        width: 100% !important;
        align-items: flex-start;
        box-sizing: border-box;
    input,select,textarea {
        color: darkslategray;
        background-color: white;
        background-clip: padding-box;
        line-height: 1.5;
        border-radius: .5rem;
        border: 1px solid #ced4da;
        transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
    input:focus, select:focus,textarea:focus {
        border-color: royalblue;
        box-shadow: none;
        -webkit-box-shadow: none;
    button {
        color: white;
        background-color: royalblue;
        border-color: royalblue;
        user-select: none;
        line-height: 1.5;
        border-radius: .5rem;
        border: 1px solid transparent;
        transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
    button:hover {
        filter: brightness(80%); 
    select:required:invalid {
  		color: silver;
        color: silver;
	option[value=""][disabled] {
  		display: none;
        color: silver;
	option {
		color: black;

CSS is its own bottomless pit of knowledge. I just taught myself, which is why some pros might not think the above is very clean. The way I taught myself was going to web pages I like, hovering over bits and typing "Inspect". Then, I modify the code in the page and see what happens. Try it!

Make the form do something with JavaScript and Zapier

The second thing to do is to make your HTML form send the information somewhere.

Rather than write long, complicated programs with RESTful APIs (even though you can do that in Google Sheets), I like to use Zapier. Zapier is a bit of software that lets you connect things around the internet. I pay $20/mo for a basic subscription and it lets me do all kinds of magic without writing code.

There's three things the form does

  1. Validates the form. This makes sure the user has filled in all the fields — except for the sneaky one to catch robots.
  2. Send the data. The form sends the data to a URL I set up in Zapier. But you can send it anywhere, if you have another place that can capture form data.
  3. Alert the user what's happening. I use the formAlert function to modify the HTML in the responsemsg span.

Here's the JavaScript (which is between two <script> tags):

    function processForm() {
        if (validateForm() === true) {
        } else {
            formAlert("Please fill out all the fields.")
    function sendData() {
        formAlert("One second...");
        var postURL = "https://hooks.zapier.com/hooks/catch/1963766/vgpqn6/";
        var http = new XMLHttpRequest();
        http.open("POST", postURL, true);
        http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        var params = "name=" + document.forms["contact-form"]["name"].value +
            "&email=" + document.forms["contact-form"]["email"].value +
            "&subject=" + document.forms["contact-form"]["subject"].value +
            "&message=" + document.forms["contact-form"]["message"].value +
            "&source_url=" + window.location.href;
        http.onload = function() {
            formAlert("Thank you, your message has been sent!");

    function validateForm() {
        var returner = false;
        var x = document.forms["contact-form"]["name"].value.length * document.forms["contact-form"]["email"].value.length * document.forms["contact-form"]["message"].value.length;
        var robotTextLength = document.forms["contact-form"]["_norobots"].value.length;
        if (x !== 0 && robotTextLength === 0) {
            returner = true;
        return returner;
    function formAlert(text) {
        document.getElementById("responsemsg").innerHTML = "<br><p><em>" + text + "</em></p>";

The HTML for the form

This is the easiest part. But it only makes sense if you know the stuff above.

OK, now we can actually make the form!

The code is fairly self-explanatory. It's only slightly more complicated than the most most basic forms you'll find on w3schools.

The only bits worth mentioning:

  • I put "placeholders" rather than labels. It's just more compact/easier to format.
  • There's a hidden input to capture robot form fillers.
  • The button has an onclick() function that does two things: a) calls processForm(), and b) prevents a new page from opening with the results.
  • There's a span element which is where I send response messages.
<h4>Contact me</h4>
<form id="contact-form" method="post" data-format="inline">
    <input type="text" id="name" placeholder="Your name" required>
    <input type="text" id="email" placeholder="your@email.com" required>
    <select id="subject" required>
        <option value="" disabled selected>Select an option</option>
        <option value="compliment">I want to compliment you, you interesting person</option>
        <option value="insult">I want to insult you, jerk!</option>
        <option value="Media">I would like to paint you</option>
        <option value="Writing">Where am I</option>
        <option value="Other">Operator</option>
    <textarea rows="5" id="message" placeholder="Message" required></textarea>
    <input type="text" name="_norobots" style="display:none !important;">
    <button class="btn" type="submit"  id="submit" value="Send" onclick="processForm();return false;">Send</button>
    <span id="responsemsg"></span>

Catch the form information with Zapier (and do whatever you want!)

Zapier is a tool that lets you connect online services.

Online services are anything that send and receive information. In this case, the two things I'm going to connect are 1.the form above and 2. email.

I'm going to make a tool that receives a form, then sends an email to me, and also to the person who filled out the form (for their records).

Zapier + Forms in Ghost blogs for easy contactforms
Using Zapier to catch HTML forms to send an email

Most of Zapier is very self explanatory (or just takes poking around). The only bit that's slightly non-obvious is the first step, "Catch Form Hook".

To create this trigger, you first create a new Zap. Your trigger (that starts the Zap's process) is part of "Webhooks by Zapier".

Choose Webhooks by Zapier to get Ghost contact form information
Webhooks by Zapier to receive the contact form info

Then, within Webhooks, you choose "Catch Hook".

The Zapier Catch Hook trigger, to get Ghost HTML contact form information
"Catch Hook" trigger to receive HTML form information

On the next page, Zapier will give you a custom URL for your webhook. This is where your form sends data (look at the HTML form above to see what mine is). It will look something like https://hooks.zapier.com/hooks/catch/1234567/a1b2c3d4/.

Take that URL and put it in your HTML and use that to test your Zap's trigger.

The form in action

Here's the form in action. Use it! (But if you're going to insult me, please be gentle.)

By the way, if you have a suggestion on how to clean up ANY of the code, please tell me. I'm totally self-taught.

Contact me