Feedback panel
MedianFeedback props, how a note is filed as a bug or a suggestion, and the API routes for notes collected elsewhere.
import { MedianFeedback } from "@mediansh/widget";
<MedianFeedback apiKey="median_pk_...">
<button>Share feedback</button>
</MedianFeedback>;
A visitor writes a note and presses Send. A model files it as a bug or a suggestion in Signal, or drops it as spam. Mounted alongside MedianSupport, the two share a visitor, so a person who writes in and leaves a note is one customer.
Props
apiKey?string
The browser-safe median_pk_ id, derived from MEDIAN_KEY with publicKeyFromMedianKey. Required unless identity is set.
stringidentity?string
Path to a route built with medianIdentity. The same prop MedianSupport takes. See Identity.
stringchildren?ReactNode
Your trigger. Rendered where you put it, inside an inline span that takes the click. A press opens the panel, a second press closes it. With identity and no apiKey, the trigger renders once the identity route answers, and not at all if the route fails. Pass apiKey too to show it at once.
ReactNodeanchor?string
A CSS selector for an element to place the panel against, when children cannot be used. Ignored when children are passed.
stringuser?MedianUser
The signed in user, if any. Without it, the note belongs to this browser's visitor.
MedianUsertelemetry?boolean
Send browser context with the note: time zone, language, browser, OS, device, screen, and the page it was written on.
booleantruescreenshot?boolean
Attach a picture of the page when the note is sent. The panel and anything else Median renders are left out.
booleantruediagnostics?boolean | { errors?: boolean; network?: boolean; console?: boolean }
Attach uncaught errors, and optionally failed requests and console.error calls, to the note.
boolean | { errors?: boolean; network?: boolean; console?: boolean }falseappState?Record<string, string | number | boolean>
Facts about your app to send with the note. Read when the note is sent. Sent only when diagnostics is on.
Record<string, string | number | boolean>open?boolean
Hold the open state yourself. See Control from code.
booleanonOpenChange?(open: boolean) => void
Called when the panel's own controls open or close it. Not called by medianFeedback.open() and the other control functions.
(open: boolean) => voidwidth?string
Any CSS length. The panel never grows wider than the window minus 2rem.
string"20rem"title?string
string"Share feedback"description?string
string"Bugs, ideas, anything."placeholder?string
string"Tell us what could be better."MedianUser, telemetry, screenshot, diagnostics and appState work as they do on MedianSupport. See Context and diagnostics. To open the panel from a menu or a shortcut, see Control from code.
Where a note goes
The panel says thanks as soon as the note is saved. A model reads it afterwards.
| Verdict | What happens |
|---|---|
| Bug | Filed on the bug list with a title, a write-up and a priority |
| Suggestion | Filed on the suggestion list the same way |
| Spam | Dropped. Nothing is filed and nobody is notified |
- A note about something already on the list joins that signal instead of adding a row. Ten people with the same complaint make one signal with ten reporters. Each person’s own words stay on their report.
- Spam is only what nobody could act on: adverts, links to unrelated products, keyboard mashing, and tests of whether the box sends. A rude note is not spam. Neither is a one word complaint.
- Priority is urgent, high, medium or low.
- Reading a note uses your workspace’s AI credits. See Plans.
- If the model cannot read the note, nothing is filed. The visitor has already been thanked.
Turn filing on or off
Notes are filed only while Track bugs and suggestions is on in Agent → Behavior. It is on by default. The same switch controls what the agent files from conversations.
| Switch | What the panel does |
|---|---|
| On | Takes the note and files it |
| Off | Shows “Feedback is currently unavailable. Try again later.” and keeps the draft |
| Turned off after a note was sent, before it was read | The note is not filed |
Pictures
A visitor adds pictures three ways:
- The Add a picture button, which opens the file picker for images.
- Dragging an image onto the panel. The box reads “Drop to attach” while it is over the panel.
- Pasting an image into the panel.
| Rule | Limit |
|---|---|
| Pictures per note | 6, counting the automatic screenshot |
| Size | 25 MB each |
| Types | Images only. SVG is refused |
A picture that breaks a rule stays in the panel with its reason, and Send stays off until it is removed.
| Reason on the picture | Cause |
|---|---|
| Larger than the 25 MB limit | The file is over 25 MB |
| Only pictures can go here | The file is not an image |
| This kind of file cannot be sent | The file is an SVG or another type browsers can run |
| Only 6 at a time | A seventh picture was added |
Automatic screenshot
When the visitor presses Send, the panel takes a picture of the page and adds it to the note.
| Case | Result |
|---|---|
screenshot={false} |
No picture is taken |
| The visitor already added 6 pictures | No picture is taken |
| The capture fails or takes over 6 seconds | The note is sent without it |
| The upload fails | The send stops, the reason shows, and the draft stays |
Pictures show on the report in Signal, and on the Linear or GitHub issue when the signal is pushed to one.
Placement
| Setup | Where the panel opens |
|---|---|
children |
Against your trigger |
anchor, no children |
Against the element the selector matches, looked up each time the panel is placed |
Neither, or anchor matches nothing |
The bottom right corner of the window |
Against a trigger or an anchor:
- The panel opens 8px below it, or above it when there is not enough room below and more room above.
- Its left edge lines up with the trigger’s left edge. If that would run off the window, the right edges line up instead.
- It stays 16px inside the window and moves with scrolling and resizing.
The anchor is only a position. Pressing it does not open or close the panel, and closes an open panel like any press outside. An invalid selector logs Median: the anchor passed to <MedianFeedback> is {selector}, which is not a valid CSS selector. and the panel opens in the corner.
From a dropdown
Choosing a menu item closes the menu and removes the item, so the panel has nothing to sit against. Mount the panel outside the menu and point anchor at the button the menu opens from:
import { MedianFeedback, medianFeedback } from "@mediansh/widget";
<MedianFeedback apiKey="median_pk_..." anchor="#account-button" />;
<DropdownMenuItem onSelect={medianFeedback.open}>
Share feedback
</DropdownMenuItem>;
Sending
| Key | Does |
|---|---|
| Enter | Adds a new line |
| Cmd+Enter or Ctrl+Enter | Sends |
| Escape | Closes the panel |
- Send is off while the box is empty or a picture has a problem.
- After a send the panel shows “Thanks for your feedback.” and closes itself 1.6 seconds later. It opens empty next time.
- Closing the panel without sending keeps the draft until the page reloads.
- A failed send keeps the draft and the pictures and shows the reason under the box.
| Notice under the box | Cause |
|---|---|
| Feedback is currently unavailable. Try again later. | Track bugs and suggestions is off |
| Enter your feedback before sending. | The note reached the server empty |
| Still connecting. Try again in a moment. | The panel had no session yet |
| Too many requests. Wait a moment and try again. | A rate limit. See Rate limits |
A “Powered by Median” line sits beside the buttons. Hide it with Show Median branding in Settings → Billing, on a plan that allows it. See Plans.
Limits
| What | Limit |
|---|---|
| Note | 2,000 characters. The box stops there, and the server trims the note and cuts it at 2,000 |
| Pictures | 6 per note, 25 MB each, images only |
| Requests | See Rate limits |
Send notes from the CLI, API or MCP
Send notes collected outside the browser, such as app store reviews or survey answers, through the same reader. These routes wait for the verdict, usually a second or two, and return it.
median feedback submit --from user_8812 --body "The export button does nothing on my phone." --image https://files.acme.com/8812.pngcurl https://api.median.sh/v1/feedback \
-H "Authorization: Bearer median_key_..." \
-H "content-type: application/json" \
-d '{"session":"user_8812","body":"The export button does nothing on my phone.","images":["https://files.acme.com/8812.png"]}'await median.feedback.submit({
sessionId: "user_8812",
body: "The export button on the billing page does nothing on my phone.",
images: [
{ url: "https://files.acme.com/8812.png", name: "Billing page on iOS" },
],
});The HTTP route is POST /v1/feedback. It takes a Median key or an OAuth token. See Authentication. Every CLI flag is in the CLI reference.
| Field | Type | Rules |
|---|---|---|
session |
string, required | A stable id for the person, such as your user id. Notes with the same session come from one reporter. 8 to 128 characters after trimming. Otherwise the request fails with “Session tokens are 8 to 128 characters.” |
body |
string, required | The note. Trimmed and cut at 2,000 characters |
user |
{ name?, email?, avatarUrl? } |
Who wrote it |
page |
{ url?, title?, referrer? } |
Where they were. Only http and https URLs are kept, without the query string or fragment |
images |
(string | { url, name? })[] |
Picture addresses. Addresses that are not http or https, or longer than 2,000 characters, are dropped. An entry that is not a string or an object with a string url fails the request, for example "images[0]" has to be a URL or an object with a "url". So does a name that is not a string. A repeated address counts once, and only the last 6 are kept |
Over MCP, send sessionId in place of session, and pass each image as { url, name? }.
The response is { outcome, signalId }.
outcome |
Meaning | signalId |
CLI prints |
|---|---|---|---|
bug |
Filed on the bug list, or joined a signal already there | The signal’s id | Filed as a bug (<signal id>). |
suggestion |
Filed on the suggestion list, or joined a signal already there | The signal’s id | Filed as a suggestion (<signal id>). |
spam |
Dropped | null |
Read as spam, so nothing was filed. |
unread |
Nothing filed. The reader was unavailable, the note was empty, or Track bugs and suggestions is off | null |
Nothing was filed: the reader was unavailable, or filing is off for this organization. |