Skip to content

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:

html
<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…":

html
<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):

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

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