Feedback modal

A simple alternative to the element picker: a button opens a form with a message, an optional email and optional attachments.

Use the modal when you want a classic "Send feedback" form. Use the feedback widget when every report should start by pointing at something on the page. Both can be mounted together; each feedback records which one sent it.

Basic setup

Place it once, under RoastnestProvider. In cloud mode submissions, screenshots and recordings go to your dashboard.

Cloud mode
import { FeedbackModal } from "@roastnest/react";

export function Layout({ children }) {
  return (
    <>
      {children}
      <FeedbackModal />
    </>
  );
}

In self-hosted mode you receive the message and the attachment blobs.

Self-hosted mode
<FeedbackModal
  mode="self-hosted"
  onSubmit={async ({ message, email, attachments, metadata }) => {
    const form = new FormData();
    form.append("message", message);
    if (email) form.append("email", email);
    attachments.forEach((a) => form.append(a.type, a.blob));

    const res = await fetch("/api/feedback", { method: "POST", body: form });
    return res.ok; // false shows the error state
  }}
/>

Attachments

Three optional buttons sit under the message box. Each can be turned off.

Select area

Works exactly like the feedback widget: hovered elements are outlined, and a click picks one. It attaches the element on its own and the full page with that element outlined in red. The click never reaches your page, so picking a link or button does not trigger it. Esc cancels.

Screenshot

Attaches a screenshot of the full page. Widget UI is left out of the picture.

Record screen

The browser asks which screen, window or tab to share. The modal hides while the user reproduces the problem, and a floating bar shows the time and a Stop button. Recording also stops at maxDurationSec (2 minutes by default, 5 at most) or when the user ends sharing from the browser.

Where recording isn't available

The button is hidden automatically where the browser can't record the screen, such as iOS. Recordings are WebM (MP4 on Safari) and capped at 25 MB; the recorder uses a modest bitrate so two minutes stays well under that.

Options

All the knobs
<FeedbackModal
  title="Found a bug?"
  email="required"
  message={{ placeholder: "What happened?", maxLength: 1000 }}
  attachments={{
    area: true,
    screenshot: true,
    recording: { maxDurationSec: 60 },
    max: 3,
  }}
  popup={{ position: "bottom-right", backdrop: "none" }}
  triggerButton={{ label: "Feedback", placement: "left-bottom" }}
/>
PropTypeDefaultDescription
titlestring"Send feedback"Modal heading.
descriptionstringLine under the heading.
email"optional" | "required" | "hidden""optional"Never asked when the user is already known (setUser with an email, or a previous submission).
message{ placeholder?, required?, maxLength? }required, 2000The message box.
attachments{ area?, screenshot?, recording?, max? }all on, max 5recording also takes { maxDurationSec, audio }.
popup{ position?, backdrop?, closeOnBackdropClick? }center, dimAlso set on the dashboard (Customize → Feedback Widget → Feedback Modal). Props override the dashboard field by field. backdrop: "none" leaves the page usable and doesn't take focus.
hideTriggerButtonbooleanfalseOpen it only from your own UI.
triggerButton{ label?, placement?, mode?: "default" | "icon" }right-bottomThe floating button.
submitLabel / cancelLabelstringButton text.
successMessage / errorMessagestringShown after sending.
themeWidgetThemeTheming
iconsFeedbackModalIconsCustom icons
onOpen / onClose() => voidLifecycle callbacks.
onSubmitted({ trackingUrl? }) => voidAfter a successful send. trackingUrl is set in cloud mode.

Opening from your own button

Custom trigger
import { FeedbackModal, useFeedbackModal } from "@roastnest/react";

// Mount it once, without the floating button…
<FeedbackModal hideTriggerButton />

// …and open it from anywhere.
function HelpMenu() {
  const { open } = useFeedbackModal();
  return <button onClick={open}>Report a problem</button>;
}

useFeedbackModal() works in any component and returns { isOpen, open, close, toggle, setTriggerButtonVisibility }. Outside React use feedbackModalController. See Controlling widgets.

Self-hosted payload

onSubmit receives
interface FeedbackModalSubmission {
  message: string;
  email?: string;
  attachments: Array<{
    id: string;
    type: "selected-screenshot" | "full-screenshot" | "screen-recording";
    blob: Blob;
    mimeType: string;      // "image/png", "video/webm" or "video/mp4"
    durationMs?: number;   // recordings only
  }>;
  metadata: {
    device, page, screen, engine, browser, mobile, os
  };
}

In the dashboard

Each submission is a normal feedback: it lands in Feedbacks with status, priority, comments and integrations. It is labelled Feedback modal, the email becomes the reporter, screenshots show as images and recordings play inline. Use the Filters menu to show only modal feedback or only feedback with a recording.

Drafts survive a close

Closing the modal keeps the message and attachments until the page reloads. A successful send clears them.