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.
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.
React
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.
'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:
import { ContactForm } from './ContactForm'
export default function ContactPage() {
return <ContactForm />
}Vue and Nuxt
<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
<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.
---
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 when you don't need JavaScript, and fetch when you want to stay on the page.