How to Add a Contact Form a on a Static Site
Static site generators like Gatsby.js are great for building a personal blog or portfolio. You get all the performance best practices built in, plus all the modern tools that make your life as a developer easier.
But if need to add a contact form on your site, it currently doesn’t have any plugin or way to handle form submissions, right out of the box.
In this article I will show you how I added a contact form on my Gatsby.js static site using Netlify Forms.
What Options Do We Currently Have?
One of the main features that I needed on my personal website was a contact form, so that any potential client can get in touch.
But I thought that building a back-end server just for that, was maybe too much.
So I started looking for ways I could handle all the form submissions.
First I thought of using a third-party service like Formspree. They have free a plan to get started but it was kind off limited and the paid one were too expensive for my needs.
So I eventually decided to use Netlify’s form feature since I am already hosting the site there.
They offer a great starter plan with up to 100 form subsmissions a month, which was great for what I needed.
If you wanna see other services or tools that work great on static sites, this repo on Github has a great list of them.
What Do You Need To Get Started?
If you host your website on Netlify, you can start using their forms service right away.
To get started, simply add a data-netlify="true"
or a netlify
attribute to
your form, a name="form-name"
attribute —which is the name your form
will have in the Netlify panel.
This is just the starting tag from mine, the entire form is a bit longer, we’ll get to that later on.
<Form
name="contact" method="post"
action={thanksURL}
data-netlify="true" data-netlify-honeypot="botfield"
onSubmit={handleSubmit}
>
Aditional Input Field for JSX Forms
If you are adding your form to a static site generator such as Gatsby.js, you
will need to add a hidden input field with name
set to form-name
and the
value
set to the form’s name, which in my case is contact
.
This step is necessary, otherwise, your form won’t show up in your Netlify admin panel.
<input type="hidden" name="form-name" aria-hidden="true" value="contact" />
Adding Spam Protection
If you need to enable spam filtering —and you probably should, or you’ll spend your 100 form submissions a month pretty fast— Netlify gives you 2 options.
The first one, is to use a hidden “honeypot” input field which only bots trying
to submit your form will see and complete. If the Netlify bots see this field
completed, the form will just be ignored. In my case, mine is called
data-netlify-honeypot="botfield"
.
data-netlify-honeypot="botfield"
The second way you can enable spam filtering, is by using reCAPTCHA 2. If you’d like to learn how to set it up on your site, you can read this blog post from Netlify in which they explain how you can do it.
So let’s start building the form.
Handling Form Submissions
By default, when anyone submits your forms, Netlify will redirect them to a generic confirmation screen. If you’re ok with using their default screen, you’re good to go.
But you can also create a custom “Thank You” page or use a React component for when javascript is enabled in the user’s browser.
<StyledForm
name="contact"
method="post"
action={thanksURL} data-netlify="true"
data-netlify-honeypot="botfield"
onSubmit={handleSubmit}>
In my case I decided to handle both cases.
1. The User has Disabled Javascript in Her Browser
When javascript is disabled in the user's browser, she will be redirected to my
custom action={thanksURL}
page. This way I can use a custom "Thank You" page
that is with my own styling.
2. The User Has Enabled Javascript in Her Browser
When Javascript is enabled in the user’s browser, the form will be handled by
the onSubmit={handleSubmit}
function. Since this is done asynchronously, I can
improve the UX of the form by rendering a loading indicator while the form data
is sent over to Netlify’s servers.
So let’s see how to do that.
async function handleSubmit(e) {
e.preventDefault();
setShowFormLoading(true); setFormSubmitted(true);
const form = e.target;
fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: encode({
"form-name": form.getAttribute("name"),
email,
fullname: fullName,
message,
botfield: botField,
}),
})
.then(() => {
handleFormSent(); })
.catch((error) => handleFormError(error));
}
From this example, you can see I’m using the new React Hooks API, but it could be done in a similar way with classes.
The first thing the handleSubmit()
function does is to set the state value of
showFormLoading
to true
. Then, a loading indicator (spinner icon) is
rendered.
Immediately after that, it toggles the state value formSubmited
from false
to true
, so that a <EmailLoading ... />
component gets rendered on the
screen (more on that later).
Showing a Loading Indicator and Status Messages
When fetch()
returns a response, the handleFormSent()
function will be
executed which will set the state variable formLoading
to false
and the
showFormSuccess
variable to true
—if there weren’t any errors returned.
function handleFormSent() {
setShowFormLoading(false);
setShowFormSuccess(true);
}
If an error is returned, the handleFormError()
function will run inside the
catch()
block which will set the state variable showFormError
to true
, so
that it renders an error message.
function handleFormSent() {
setShowFormLoading(false);
setShowFormSuccess(true);
}
function handleFormError(error) {
setShowFormLoading(false);
setShowFormError(true);
setFormErrorRes(error);
}
While the form data is sent over to Netlify’s servers, a spinner icon replaces the copy inside the submit button.
Then, when formSubmitted
is set to true
, the <EmailLoading />
component
will render and it will display the loading indicator, together with the
success, or error messages, on the screen.
{
formSubmitted && (
<EmailLoading
showFormLoading={showFormLoading}
showFormSuccess={showFormSuccess}
showFormError={showFormError}
formErrorRes={formErrorRes}
locale={locale}
/>
);
}
The <EmailLoading />
component simply returns a loading indicator when
isLoading
is true
and a success/error message when the response is returned.
{
showFormLoading ? (
<Spinner />
) : (
<>
{FORM_SUBMIT_STATUS.cta[locale]}
<StyledIcon aria-hidden="true">
<use xlinkHref="#correct" />
</StyledIcon>
</>
);
}
Validating the Form with CSS
In order to handle the form validation, I decided to use only CSS.
My solution is based on using CSS Level 3 selectors such as &:valid
, :not()
,
:placeholder-shown
or :invalid
.
If you’d like to learn more about them, you can read this article from css-tricks.
By using a combination of these CSS selectors, I could even get real-time validation while users are typing the information in the form fields.
For example, this is how you could validate the data in the input field when it is currently not focused, data has been entered and it's valid.
/* Input is:
* 1. Valid
* 2. NOT Empty
* 3. NOT In Focus
*/
&:valid:not(:focus):not(:placeholder-shown) {
color: ${theme.colors.success600};
background-color: white !important;
border: 2px solid ${theme.colors.success400};
padding-right: ${rem(40)};
& ~ span {
display: block !important;
${optimizedSuccessSVGDataURI};
background-size: ${rem(24)};
background-repeat: no-repeat;
background-position: 0px 0px;
}
}
In this case, I’m also showing an svg icon inserted with a ES6 template literal
${optimizedSuccessSVGDataURI};
as a background image, by using the css ~
selector.
Besides validating the form fields, you should also make sure that you’re using the wright attributes on each input field.
In my example, I use the email
input type, which has auto capitalization
disabled with autoCapitalize=off
and auto correction disabled with
autoCorrect=off
.
I also set the autoComplete
attribute to email
, so that the user's browser
can autocomplete her email faster without having to type it.
<StyledInput
type="email"
value={email}
name="email"
placeholderType="email"
title={INPUT_EMAIL_ERROR[locale]}
pattern="^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$"
autoCapitalize="off"
autoCorrect="off"
`autoComplete`="email"
onChange={(e) => setEmail(e.target.value)}
required
/>
The regular expression used in the pattern
fields is used to test if the
entered email is correct, particularly the part after the @ that contains the
domain name.
The value stored in the title
attribute will be shown when the input field
isn’t valid. However, each browser shows this message differently, so test it
first.
The INPUT_EMAIL_ERROR[locale]
simply stores a localized version of the title
since this blog is translated into spanish and english.
Making the Contact Form GDPR Compliant
Since I live in Europe, I have to be GDPR compliant. Therefore, I needed some extra form fields and logic. The main input field that is missing is a consent checkbox which has to be enabled by the user before she can submit the form.
This is also pretty straightforward to do with Netlify forms since you can send over the values stored in each input field. You can also save the user’s consent, which you’ll need to prove that they gave you their explicit consent to use their personal data.
Note:
Before you continue reading, you should note that by simply following these steps, you won’t be fully GDPR compliant. There’s much more involved with being GDPR compliant, which I won’t show here.
To be sure, you should talk to your own lawyer like I did.
Disabling the Submit Button
According to the current privacy laws in Europe, users can’t send you their personal information before giving you their consent to use or store their personal information.
So I had to add a checkbox which isn’t checked by default— that controls if the form can be submitted.
This was pretty easy to do by using a boolean state variable that is set to
true
when the checkbox is enabled. Otherwise, the button is disabled by
default, until the checkbox is enabled.
{
!formSubmitted && <SubmitButton disabled={!acceptsConsentCheckbox} />;
}
Then in your Netlify forms panel, you should see all the form submissions, with the user’s consent value.
The Final Version of the Contact Form
function ContactForm({ locale }) {
const [email, setEmail] = useState("");
const [fullName, setFullName] = useState("");
const [message, setMessage] = useState("");
const [botField, setBotField] = useState("");
const [acceptsConsentCheckbox, setAcceptsConsentCheckbox] = useState(false);
const [checkboxValue, setCheckboxValue] = useState(CONSENT_VALUE[locale].no);
const [formSubmitted, setFormSubmitted] = useState(false);
const [showFormLoading, setShowFormLoading] = useState(false);
const [showFormSuccess, setShowFormSuccess] = useState(false);
const [showFormError, setShowFormError] = useState(false);
const [formErrorRes, setFormErrorRes] = useState({});
function encode(data) {
return Object.keys(data)
.map(
(key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]),
)
.join("&");
}
async function handleSubmit(e) {
e.preventDefault();
setShowFormLoading(true);
setFormSubmitted(true);
const form = e.target;
fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: encode({
"form-name": form.getAttribute("name"),
email,
fullname: fullName,
message,
datesent: dateSent,
botfield: botField,
acceptsconsentcheckbox: acceptsConsentCheckbox,
}),
})
.then(() => {
handleFormSent();
})
.catch((error) => handleFormError(error));
}
function handleConsentCheckbox(e) {
setAcceptsConsentCheckbox(e.target.checked);
if (e.target.checked) {
setCheckboxValue(CONSENT_VALUE[locale].yes);
} else {
setCheckboxValue(CONSENT_VALUE[locale].no);
}
}
function handleFormSent() {
setShowFormLoading(false);
setShowFormSuccess(true);
}
function handleFormError(error) {
setShowFormLoading(false);
setShowFormError(true);
setFormErrorRes(error);
}
return (
<FormContainer>
<StyledForm
name="contact"
method="post"
action="path-to-custom-thank-you-page"
data-netlify="true"
data-netlify-honeypot="botfield"
onSubmit={handleSubmit}
>
<input
type="hidden"
name="form-name"
aria-hidden="true"
value="contact"
/>
<input
style={{ display: "none" }}
aria-hidden="true"
name="botfield"
value={botField}
onChange={(e) => setBotField(e.target.value)}
/>
<StyledLabel labelType="full name">
<StyledInput
type="text"
value={fullName}
name="fullname"
placeholderType="full name"
autoCorrect="off"
autoComplete="name"
onChange={(e) => setFullName(e.target.value)}
required
/>
<InputStatusIcon aria-hidden="true" />
</StyledLabel>
<StyledLabel labelType="email">
<StyledInput
type="email"
value={email}
name="email"
placeholderType="email"
title={INPUT_EMAIL_ERROR[locale]}
pattern="^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$"
autoCapitalize="off"
autoCorrect="off"
autoComplete="email"
onChange={(e) => setEmail(e.target.value)}
required
/>
<InputStatusIcon aria-hidden="true" />
</StyledLabel>
<StyledLabel labelType="message">
<StyledTextArea
rows="8"
value={message}
name="message"
onChange={(e) => setMessage(e.target.value)}
required
/>
<InputTextAreaStatusIcon aria-hidden="true" />
</StyledLabel>
<PrivacyCheckbox
type="checkbox"
name="acceptsconsentcheckbox"
value={checkboxValue}
onChange={handleConsentCheckbox}
locale={locale}
required
/>
{formSubmitted && (
<EmailLoading
showFormLoading={showFormLoading}
showFormSuccess={showFormSuccess}
showFormError={showFormError}
formErrorRes={formErrorRes}
locale={locale}
/>
)}
{!formSubmitted && <SubmitButton disabled={!acceptsConsentCheckbox} />}
</StyledForm>
</FormContainer>
);
}
Final Thoughts
Adding a contact form on a static site generator like Gatsby.js turned out to be easier than I thought.
By using a service such as Netlify, it’s pretty straightforward to build, and it can also be GDPR compliant.
Hope it helped you learn how to add one on your site.
Feel free to comment bellow if you know any alternatives to using Netlify. So far I’m happy with it, but it can always be improved 🤔.