Get started

The form (React)

tsx
import { useBugReport } from "bugbottle/react";
import { htmlToImage } from "bugbottle/html-to-image"; // optional

function ReportForm() {
  const report = useBugReport({
    endpoint: "/api/feedback",
    screenshot: htmlToImage, // leave out to disable screenshots
  });

  return (
    <form data-bugbottle onSubmit={(e) => { e.preventDefault(); void report.submit(); }}>
      {report.types.map((t) => (
        <button key={t} type="button" onClick={() => report.setType(t)}>
          {t}
        </button>
      ))}

      <textarea
        value={report.message}
        onChange={(e) => report.setMessage(e.target.value)}
      />

      {report.canScreenshot && (
        <label>
          <input
            type="checkbox"
            checked={report.includeScreenshot}
            onChange={(e) => report.toggleScreenshot(e.target.checked)}
          />
          Attach a picture of this page
        </label>
      )}

      {report.screenshot && <img src={report.screenshot} alt="" />}

      <button type="button" onClick={() => void report.pickElement()}>
        {report.isPicking ? "Click anything to attach it — Esc to stop" : "Point at the element"}
      </button>
      <ul>
        {report.elements.map((el, i) => (
          <li key={i}>
            <code>{el.selector}</code> {el.text}
            <button type="button" onClick={() => report.removeElement(i)}>×</button>
          </li>
        ))}
      </ul>

      <p role="status">{report.statusMessage}</p>
      <button disabled={report.isSending}>Send</button>
    </form>
  );
}

Call report.open() when the form appears, so the screenshot shows what they were looking at rather than the form on top of it. Anything marked data-bugbottle is left out of the picture and cannot be picked — put it on your panel and your trigger button.

Every adapter also carries contact and setContact, for a form that asks how to reach the reporter — an ordinary input bound the way message is:

tsx
<input
  type="email"
  value={report.contact}
  onChange={(e) => report.setContact(e.target.value)}
/>

Nothing validates it, and an empty one is left out of the body entirely, so a form without such a field sends no contact key at all. In Vue it is a writable ref (v-model="contact"), in Svelte $form.contact with form.setContact(…), in Solid the accessor contact(). It is personal data once you ask for it: see Please read this part.

Other options: initialType, screenshotFor and consoleFor (which report types get a picture and the console; bugs only by default), extra (fields merged into the body — an app version, a tenant id), headers and credentials (for an authenticated or cross-origin endpoint), timeoutMs, onSent, parseError, and messages for translated strings. The defaults are English.

Edit this page on GitHub