# React, Next.js, Vue, Svelte and Astro

Copy-paste form components for React, Next.js, Vue, Nuxt, Svelte, SvelteKit and Astro that submit to Nisuform.

Every framework works the same way: collect the fields, `POST` them to your endpoint and read `ok` from the JSON response. You don't need API routes, server actions or environment variables, because the form key is safe to publish.

<Callout title="Generated for your form">
  The form's **Setup** tab, under **Code**, has these components ready with your endpoint and field names filled in. **Your design** gives you the styled form from the builder as a component. **Your own styles** gives you a bare component to style your way.
</Callout>

## React

```jsx title="ContactForm.jsx"
import { useRef, useState } from 'react'

export function ContactForm() {
  const shownAt = useRef(Date.now())
  const [status, setStatus] = useState('idle')

  async function onSubmit(event) {
    event.preventDefault()
    setStatus('sending')
    const data = Object.fromEntries(new FormData(event.currentTarget))
    data._elapsed = String(Date.now() - shownAt.current)
    const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
      body: JSON.stringify(data),
    })
    setStatus(response.ok ? 'sent' : 'error')
  }

  if (status === 'sent') {
    return <p>Thanks! We got your message.</p>
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" placeholder="Name" required />
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" rows={5} required />
      <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: 'none' }} />
      <button type="submit" disabled={status === 'sending'}>Send</button>
      {status === 'error' && <p role="alert">Something went wrong. Please try again.</p>}
    </form>
  )
}
```

`Object.fromEntries` keeps only the last value of fields that share a name. For checkbox groups, send the `FormData` itself instead of JSON.

## Next.js

Forms need state and event handlers, so mark the component as a client component. It works in both the App Router and the Pages Router.

```jsx title="app/contact/ContactForm.jsx"
'use client'

import { useRef, useState } from 'react'

export function ContactForm() {
  const shownAt = useRef(Date.now())
  const [status, setStatus] = useState('idle')

  async function onSubmit(event) {
    event.preventDefault()
    setStatus('sending')
    const data = new FormData(event.currentTarget)
    data.set('_elapsed', String(Date.now() - shownAt.current))
    const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', {
      method: 'POST',
      headers: { Accept: 'application/json' },
      body: data,
    })
    setStatus(response.ok ? 'sent' : 'error')
  }

  if (status === 'sent') {
    return <p>Thanks! We got your message.</p>
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" rows={5} required />
      <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: 'none' }} />
      <button type="submit" disabled={status === 'sending'}>Send</button>
      {status === 'error' && <p role="alert">Something went wrong. Please try again.</p>}
    </form>
  )
}
```

Then render it from any page:

```jsx title="app/contact/page.jsx"
import { ContactForm } from './ContactForm'

export default function ContactPage() {
  return <ContactForm />
}
```

## Vue and Nuxt

```vue title="ContactForm.vue"
<script setup>
import { ref } from 'vue'

const shownAt = Date.now()
const status = ref('idle')

async function onSubmit(event) {
  status.value = 'sending'
  const data = new FormData(event.target)
  data.set('_elapsed', String(Date.now() - shownAt))
  const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', {
    method: 'POST',
    headers: { Accept: 'application/json' },
    body: data,
  })
  status.value = response.ok ? 'sent' : 'error'
}
</script>

<template>
  <p v-if="status === 'sent'">Thanks! We got your message.</p>
  <form v-else @submit.prevent="onSubmit">
    <input type="email" name="email" placeholder="Email" required />
    <textarea name="message" rows="5" required></textarea>
    <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display: none" />
    <button type="submit" :disabled="status === 'sending'">Send</button>
    <p v-if="status === 'error'" role="alert">Something went wrong. Please try again.</p>
  </form>
</template>
```

In Nuxt, put the file in `components/` and use it as `<ContactForm />` on any page.

## Svelte and SvelteKit

```svelte title="ContactForm.svelte"
<script>
  const shownAt = Date.now()
  let status = $state('idle')

  async function onsubmit(event) {
    event.preventDefault()
    status = 'sending'
    const data = new FormData(event.currentTarget)
    data.set('_elapsed', String(Date.now() - shownAt))
    const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', {
      method: 'POST',
      headers: { Accept: 'application/json' },
      body: data,
    })
    status = response.ok ? 'sent' : 'error'
  }
</script>

{#if status === 'sent'}
  <p>Thanks! We got your message.</p>
{:else}
  <form {onsubmit}>
    <input type="email" name="email" placeholder="Email" required />
    <textarea name="message" rows="5" required></textarea>
    <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display: none" />
    <button type="submit" disabled={status === 'sending'}>Send</button>
    {#if status === 'error'}
      <p role="alert">Something went wrong. Please try again.</p>
    {/if}
  </form>
{/if}
```

## Astro

Astro pages are static by default, and a plain HTML form works with zero client-side JavaScript. The browser posts it straight to Nisuform and shows the thank-you page.

```astro title="src/pages/contact.astro"
---
const endpoint = 'https://api.nisuform.com/s/YOUR_FORM_KEY'
---

<form action={endpoint} method="POST">
  <label>
    Email
    <input type="email" name="email" required />
  </label>
  <label>
    Message
    <textarea name="message" rows="5" required></textarea>
  </label>
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display: none" />
  <button type="submit">Send</button>
</form>
```

To paste the styled embed into an Astro page, keep its `<style>` and `<script>` tags as they are. The Setup tab's Astro snippet adds `is:inline` so Astro does not bundle the script or scope the styles.

## Other frameworks

The same approach works in Remix, React Router, SolidJS, Angular, Qwik, Hugo, Jekyll, Eleventy and anything else that renders HTML. Use the [HTML form](/docs/connect/html) when you don't need JavaScript, and [fetch](/docs/connect/javascript) when you want to stay on the page.
