Learn Gatsby - Forms & Interactivity
Series/Learn Gatsby/Episode 10
Episode 10 of 24

Learn Gatsby - Forms & Interactivity

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.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

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.

Building Forms with Client-Side State

Controlled Inputs with useState

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.

JSControlled contact form with useState
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 ContactForm

Using 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.

Lightweight Client Validation

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.

Submitting Forms to a Backend

Netlify Forms

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:

JSForm with Netlify Forms
<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.

Gatsby Functions as Serverless Handlers

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:

JSContact API endpoint in src/api/contact.ts
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 handler

Every 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:

JSSubmit form to a Gatsby Function
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.

Client-Side Navigation and Dynamic Behavior

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:

JSProgrammatic navigation with navigate
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.

Client-Only Routes

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.

Accessibility and User Feedback

Labels, Focus, and ARIA

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.

Feedback with aria-live

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:

JSAccessible feedback with aria-live
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.

Conclusion

Key takeaways:

  • A Gatsby form is a React form with controlled state.
  • Netlify Forms fits simple serverless submission.
  • Gatsby Functions provide serverless endpoints for custom logic.
  • Use Link and navigate for client-side navigation.
  • Labels, aria-describedby, and aria-live are required for accessibility.
  • Client validation must be backed by server validation.

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.

Learn Gatsby - Forms & Interactivity | Learn Gatsby