Vanilla JS
Every example on this page uses plain DOM APIs and fetch: no library, no build step. 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, which works with JavaScript disabled entirely:
<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 for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="message">Message</label>
<textarea id="message" name="message"></textarea>
<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 the same data with fetch, and update the page in place instead of navigating away. The try/catch covers a failed network request, so the page never stays stuck on "Sending…":
<form id="contact-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 for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="message">Message</label>
<textarea id="message" name="message"></textarea>
<button type="submit">Send</button>
</form>
<p id="contact-status" role="status"></p>
<script>
document.getElementById('contact-form').addEventListener('submit', async (event) => {
event.preventDefault()
const form = event.target
const button = form.querySelector('button[type="submit"]')
const status = document.getElementById('contact-status')
button.disabled = true
status.textContent = '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()
status.textContent = 'Thanks, your message was sent.'
} else {
status.textContent = result.error
}
} catch {
status.textContent = 'Could not send. Check your connection and try again.'
} finally {
button.disabled = false
}
})
</script>File upload
FormData handles files the same way as text fields: it picks up the file input's contents with no extra code. Don't set Content-Type yourself (the browser sets the multipart boundary):
<form id="upload-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 for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="resume">Resume</label>
<input id="resume" name="resume" type="file" required />
<button type="submit">Send</button>
</form>
<p id="upload-status" role="status"></p>
<script>
document.getElementById('upload-form').addEventListener('submit', async (event) => {
event.preventDefault()
const form = event.target
const status = document.getElementById('upload-status')
status.textContent = '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()
status.textContent = result.ok ? 'Thanks, your file was sent.' : result.error
} catch {
status.textContent = 'Could not send. Check your connection and try again.'
}
})
</script>A file upload requires the form to be claimed first, or you get 422 FORM_NOT_CLAIMED. See Building your form.
Moderated content feed
Fetch a form's approved submissions and let visitors react, with no dashboard/API key involved; this endpoint is public and unauthenticated by design. A form without a feed (unclaimed, below Pro, or a mistyped id) answers with an error status instead, so the script checks response.ok first:
<ul id="feed"></ul>
<script>
async function loadFeed() {
const response = await fetch('https://f.bootform.com/forms/{form_id}/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()
const list = document.getElementById('feed')
list.replaceChildren(
...items.map((item) => {
const li = document.createElement('li')
const message = document.createElement('p')
message.textContent = item.fields.message // textContent, never innerHTML: values are visitor input
const up = document.createElement('button')
up.textContent = `👍 ${item.reactions.up}`
up.addEventListener('click', () => react(item.id, 'up'))
const down = document.createElement('button')
down.textContent = `👎 ${item.reactions.down}`
down.addEventListener('click', () => react(item.id, 'down'))
li.append(message, up, down)
return li
}),
)
}
async function react(submissionId, type) {
await fetch(`https://f.bootform.com/forms/{form_id}/feed/${submissionId}/react`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type }),
})
loadFeed() // refetch to show the updated count
}
loadFeed()
</script>See Moderated content feed for the report endpoint, pagination, and the hosted-widget alternative if you'd rather not write this yourself.