React
Every example on this page is a self-contained React component using fetch, with no BootForm SDK to install. They're written in TypeScript (.tsx), as in Vite's default React template; for plain JavaScript, drop the type annotations. Jump to just the one you need from the outline on the right, or read top to bottom for the full picture.
Basic submission
The plain HTML case works unchanged inside JSX, with no state needed at all:
export default function ContactForm() {
return (
<form action="https://f.bootform.com/{form_id}" method="POST">
{/* Required: BootForm's spam trap. Keep this hidden field and leave it empty. */}
<input type="text" name="honeypot" style={{ position: 'absolute', left: '-9999px' }} tabIndex={-1} autoComplete="off" aria-hidden="true" />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" />
<button type="submit">Send</button>
</form>
)
}The hidden honeypot input is BootForm's spam trap. New forms use the name honeypot; if you renamed yours, use the name in your form's snippet (in the dashboard, or from bootform_get_snippet over MCP). See Spam protection.
AJAX submission
Intercept the submit, post with fetch, and track status with useState instead of navigating away. The try/catch covers a failed network request, so the button never stays stuck on "Sending…":
import { useState, type FormEvent } from 'react'
export default function ContactForm() {
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
const [error, setError] = useState<string | null>(null)
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
const form = event.currentTarget
setStatus('sending')
try {
const response = await fetch(form.action, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form),
})
const result = await response.json()
if (result.ok) {
form.reset()
setStatus('sent')
} else {
setError(result.error)
setStatus('error')
}
} catch {
setError('Could not send. Check your connection and try again.')
setStatus('error')
}
}
return (
<form action="https://f.bootform.com/{form_id}" method="POST" onSubmit={handleSubmit}>
{/* Required: BootForm's spam trap. Keep this hidden field and leave it empty. */}
<input type="text" name="honeypot" style={{ position: 'absolute', left: '-9999px' }} tabIndex={-1} autoComplete="off" aria-hidden="true" />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" />
<button type="submit" disabled={status === 'sending'}>
{status === 'sending' ? 'Sending…' : 'Send'}
</button>
{status === 'sent' && <p role="status">Thanks, your message was sent.</p>}
{status === 'error' && <p role="alert">{error}</p>}
</form>
)
}File upload
FormData picks up a file input the same way as text fields; nothing React-specific needed beyond the state you're already tracking above:
import { useState, type FormEvent } from 'react'
export default function UploadForm() {
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
const [error, setError] = useState<string | null>(null)
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
const form = event.currentTarget
setStatus('sending')
try {
const response = await fetch(form.action, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form), // includes the selected file automatically
})
const result = await response.json()
setError(result.ok ? null : result.error)
setStatus(result.ok ? 'sent' : 'error')
} catch {
setError('Could not send. Check your connection and try again.')
setStatus('error')
}
}
return (
<form action="https://f.bootform.com/{form_id}" method="POST" onSubmit={handleSubmit}>
{/* Required: BootForm's spam trap. Keep this hidden field and leave it empty. */}
<input type="text" name="honeypot" style={{ position: 'absolute', left: '-9999px' }} tabIndex={-1} autoComplete="off" aria-hidden="true" />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="resume">Resume</label>
<input id="resume" name="resume" type="file" required />
<button type="submit" disabled={status === 'sending'}>
{status === 'sending' ? 'Sending…' : 'Send'}
</button>
{status === 'sent' && <p role="status">Thanks, your file was sent.</p>}
{status === 'error' && <p role="alert">{error}</p>}
</form>
)
}A file upload requires the form to be claimed first, or you get 422 FORM_NOT_CLAIMED. See Building your form.
Moderated content feed
Load a form's approved submissions on mount, and let visitors react. The feed endpoint is public and unauthenticated, no API key involved. A form without a feed (unclaimed, below Pro, or a mistyped id) answers with an error status instead, so the component checks response.ok and keeps the list empty rather than crashing:
import { useEffect, useState } from 'react'
interface FeedItem {
id: string
fields: Record<string, string>
reactions: { up: number; down: number }
}
async function fetchFeed(formId: string): Promise<FeedItem[]> {
const response = await fetch(`https://f.bootform.com/forms/${formId}/feed?per_page=20`)
if (!response.ok) return [] // no feed for this form: unclaimed, below Pro, or a wrong id
const { items } = await response.json()
return items
}
export default function ContentFeed({ formId }: { formId: string }) {
const [items, setItems] = useState<FeedItem[]>([])
useEffect(() => {
fetchFeed(formId).then(setItems)
}, [formId])
async function react(submissionId: string, type: 'up' | 'down') {
await fetch(`https://f.bootform.com/forms/${formId}/feed/${submissionId}/react`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type }),
})
setItems(await fetchFeed(formId)) // refetch to show the updated count
}
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<p>{item.fields.message}</p>
<button onClick={() => react(item.id, 'up')}>👍 {item.reactions.up}</button>
<button onClick={() => react(item.id, 'down')}>👎 {item.reactions.down}</button>
</li>
))}
</ul>
)
}See Moderated content feed for the report endpoint, pagination, and the hosted-widget alternative if you'd rather not write this yourself.