This episode covers forms and interactivity in Gatsby: forms with client-side state, submissions via Netlify Forms and Gatsby Functions, client-side navigation, and patterns for accessibility and user feedback.

A static site doesn't mean totally static. Pages generated at build time can still host a contact form, product search, or data filters that run fully in the browser. Gatsby provides React on the client side and Gatsby Functions on the server side, so the combination delivers complete interactions.
Episode 10 covers forms with client-side state, form submissions via Netlify Forms and Gatsby Functions, client-side navigation and dynamic behavior, and finally accessibility and good user feedback patterns.
A form in Gatsby is essentially a React form. Each input keeps its value in state, and that state is what's used for validation and submission. After the hydration process finishes, all handlers work normally just like in a regular React app.
import { useState } from "react"
const ContactForm = () => {
const [values, setValues] = useState({ name: "", email: "", message: "" })
const handleChange = (event) => {
const { name, value } = event.target
setValues((prev) => ({ ...prev, [name]: value }))
}
const handleSubmit = (event) => {
event.preventDefault()
console.log(values)
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Nama</label>
<input id="name" name="name" onChange={handleChange} />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" onChange={handleChange} />
<label htmlFor="message">Pesan</label>
<textarea id="message" name="message" onChange={handleChange} />
<button type="submit">Kirim</button>
</form>
)
}
export default ContactFormUsing the same name attribute on every input lets the handleChange handler be reused across all fields. This is what's called a controlled component: the input's value comes from state, not the other way around.
Validation can be done before submission by checking the state's contents. But client-side validation alone isn't enough — the form must still be validated on the server. The handleSubmit above only prints data to the console; in the next section we'll replace it with a real submission.
Netlify Forms lets a Gatsby form be submitted without writing a server. When you deploy to Netlify, the built HTML files are scanned and forms meeting the rules automatically become endpoints. Add a name attribute to the form and some hidden fields:
<form
name="contact"
method="POST"
data-netlify="true"
netlify-honeypot="bot-field"
>
<input type="hidden" name="form-name" value="contact" />
<input type="hidden" name="bot-field" />
<input type="text" name="name" placeholder="Nama" />
<input type="email" name="email" placeholder="Email" />
<textarea name="message" placeholder="Pesan" />
<button type="submit">Kirim</button>
</form>When the user submits, the browser sends a POST directly to Netlify and the submission appears in the Netlify dashboard. Netlify Forms is best for sites that are actually hosted on Netlify and don't need extra server logic.
If you need custom logic — advanced validation, email notifications, or CRM integration — Gatsby Functions provide serverless endpoints inside your project. Save a file in the src/api folder:
import type { GatsbyFunctionRequest, GatsbyFunctionResponse } from "gatsby"
const handler = (request: GatsbyFunctionRequest, response: GatsbyFunctionResponse) => {
const { name, email, message } = request.body
if (!email || !message) {
response.status(400).json({ error: "Email dan pesan wajib diisi" })
return
}
response.status(200).json({ ok: true, received: { name, email } })
}
export default handlerEvery file in src/api automatically becomes an endpoint at /api/{file-name} with no extra configuration. In development, Gatsby Functions run alongside gatsby develop; in production, they're distributed as serverless functions according to your hosting platform.
From the form component, send the data with fetch:
const handleSubmit = async (event) => {
event.preventDefault()
setStatus("loading")
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
if (res.ok) {
setStatus("success")
} else {
setStatus("error")
}
}fetch("/api/contact", ...) sends JSON to the Gatsby Function endpoint. The server's ok: true response is used to flip the UI status to success.
Navigation between pages in Gatsby uses the Link component instead of the a tag, so transitions happen on the client without a full reload. For navigation triggered by other actions, use the navigate function:
import { navigate } from "gatsby"
const handleSuccess = () => navigate("/terima-kasih")navigate("/terima-kasih") moves between pages on the client while still taking advantage of Gatsby's prefetching. Prefetching loads the destination page ahead of time, so transitions feel instant.
For pages that only exist on the client, such as a user dashboard, use the client-only route pattern with @reach/router, which ships bundled with Gatsby. Create a blank page and render a dynamic component that reads the path from the location prop.
Every input must have a label. Use htmlFor on the label and id on the input so the two are connected, letting screen readers announce the label correctly. For error messages, use aria-describedby so assistive technology reads out the relationship between the input and its error message.
Status feedback — loading, success, or error — must be announced without abruptly shifting the user's focus. Use an aria-live="polite" region, which screen readers announce when its content changes:
const FormStatus = ({ status }) => (
<div aria-live="polite">
{status === "loading" && "Mengirim data..."}
{status === "success" && "Terima kasih, pesan kalian terkirim."}
{status === "error" && "Terjadi kesalahan, coba lagi."}
</div>
)aria-live="polite" ensures screen readers announce status changes without interruption. Besides that, avoid disabling the submit button without reason, and give clear visual indicators while a button is busy processing.
Key takeaways:
Link and navigate for client-side navigation.aria-describedby, and aria-live are required for accessibility.In the next episode, episode 11, we'll discuss localization and i18n — how to internationalize a Gatsby site, multi-language routing, content translation, and SEO for multilingual pages.