Control from code
Open, close and watch the support and feedback panels from your own code, with or without React.
import { MedianSupport } from "@mediansh/widget";
<MedianSupport apiKey="median_pk_..." launcher="hidden" />;"use client";
import { useMedianSupport } from "@mediansh/widget";
export function HelpButton() {
const support = useMedianSupport();
return (
<button onClick={support.open}>
Help
{support.unreadCount > 0 && <span>{support.unreadCount}</span>}
</button>
);
}Mount the component once, near the root. Call the hook from any component. No provider is needed.
Support panel
useMedianSupport() returns the controls for MedianSupport and MedianSupportModal. medianSupport is the same set as a plain object, for code outside components.
| Name | Type | Description |
|---|---|---|
isOpen | boolean | Whether the panel is open, however it was opened. |
unreadCount | number | Replies the visitor has not read. Drops to 0 once the panel is opened and the replies are marked read. 0 while no panel is mounted. |
open | () => void | Open the panel. |
close | () => void | Close the panel. |
toggle | () => void | Open the panel if it is closed, close it if it is open. |
attach | (label: string, value: unknown) => void | Queue your own data for the next message, feedback note, contact form send or crash report. See Context and diagnostics. |
reportError | (error: unknown, options?: MedianErrorReportOptions) => Promise<MedianErrorReportOutcome> | File the crash your error screen is showing. See Crash reports. |
reset | () => void | Forget this browser's conversation and saved email. The next message starts a new, anonymous visitor. Call it on sign out. See Identity. |
useMedianSupport() | medianSupport | |
|---|---|---|
| Use in | React components | Anywhere, including code with no React |
isOpen and unreadCount | Re-render the component when they change | Read the current value. Nothing re-renders, and there is no change event |
| On the server and the first client render | isOpen is false, unreadCount is 0 | The same |
open()called before the panel mounts is kept. The panel opens as soon as it mounts.MedianSupportandMedianSupportModalshare one open state. Mount one of them.- Details for the members that are not about opening: attach, reportError, reset.
Open from your own button
Set launcher to take the round button out of the corner:
<MedianSupport apiKey="median_pk_..." launcher="hidden" />launcher | While the panel is closed |
|---|---|
"bubble" (default) | A round button in the corner, with the unread count and the unread reply card |
"hidden" | Nothing on the page |
With launcher="hidden":
- Open the panel with
open()ortoggle()from the hook ormedianSupport. - The corner panel opens where the button would be.
MedianSupportModalstill opens in the middle of the screen. - There is no count or reply card to show new replies. Put
unreadCounton your own button, as in the example at the top. The count in the tab title still works. Turn it off withtitleCount={false}. - Closing the panel returns focus to the element that had focus when it opened.
You can also keep the bubble and add your own trigger. launcher and the controls are independent.
Controlled open state
Pass open to hold the state yourself. Pass onOpenChange to hear every open and close, whatever asked for it.
const [open, setOpen] = useState(false);
<MedianSupport
apiKey="median_pk_..."
launcher="hidden"
open={open}
onOpenChange={setOpen}
/>;
<button onClick={() => setOpen(true)}>Help</button>;| What opens or closes it | Calls onOpenChange |
|---|---|
| The launcher button | Yes |
| The unread reply card | Yes |
| The close button in the panel header | Yes |
| Escape | Yes |
| A press on the modal's backdrop | Yes |
| Following a page link from a reply, on a phone or in the modal | Yes |
open(), close() and toggle() from the hook or medianSupport | Yes |
- While
openis passed, the panel shows exactlyopen. Everything in the table callsonOpenChangeand changes nothing until you updateopen. - Without the
openprop, the panel changes directly andonOpenChangestill hears about it. isOpenfrom the hook follows theopenprop.
Feedback panel
useMedianFeedback() and medianFeedback control MedianFeedback the same way. The feedback panel keeps its own state. Opening it never opens the support panel.
import { useMedianFeedback } from "@mediansh/widget";
function FeedbackMenuItem() {
const feedback = useMedianFeedback();
return <MenuItem onSelect={feedback.open}>Share feedback</MenuItem>;
}| Name | Type | Description |
|---|---|---|
isOpen | boolean | Whether the panel is open, however it was opened. |
open | () => void | Open the panel. |
close | () => void | Close the panel. |
toggle | () => void | Open the panel if it is closed, close it if it is open. |
A MedianFeedback with no children and no anchor has no trigger. It opens in the bottom right corner when you call open(). To hang it off a button inside a dropdown, see Feedback panel.
MedianFeedback takes open and onOpenChange too.
| What opens or closes it | Calls onOpenChange |
|---|---|
| The trigger you wrapped | Yes |
| The close button | Yes |
| Escape | Yes |
| A press outside the panel and the trigger | Yes |
| The panel closing itself after a note is sent | Yes |
open(), close() and toggle() from the hook or medianFeedback | Yes |
The same rules apply as for the support panel.
Without React
Each component has a mount function for pages with no React of their own: Vue, Svelte, Rails templates, plain HTML. It takes the component's props as one options object.
import "@mediansh/widget/styles.css";
import { medianSupport, mountMedianSupport } from "@mediansh/widget";
const support = mountMedianSupport({
apiKey: "median_pk_...",
launcher: "hidden",
});
document.querySelector("#help")?.addEventListener("click", () => {
medianSupport.open();
});
// When someone signs in.
support.update({ user: { name: "Ada", email: "ada@example.com" } });Install the package as described in Install the widget. The package is an ES module and imports react, react-dom and convex, so the page needs a bundler and those three installed. Your own code does not use React.
| Function | Renders | Options | Where it goes |
|---|---|---|---|
mountMedianSupport(options) | The support widget in the corner | Every MedianSupport prop | A <div data-median-support> appended to <body> |
mountMedianSupportModal(options) | The support widget as a modal | Every MedianSupportModal prop | A <div data-median-support-modal> appended to <body> |
mountMedianFeedback(options) | The feedback panel | Every MedianFeedback prop except children. It opens in the bottom right corner, or next to anchor | A <div data-median-feedback> appended to <body> |
mountMedianContactForm(options) | The contact form | Every MedianContactForm prop, plus target (required) | A <div data-median-contact-form> appended inside target |
Callbacks such as onOpenChange, onNavigate, onSignIn and onSent work as options.
The instance
Every mount function returns the same two methods.
| Name | Type | Description |
|---|---|---|
update | (options: Partial<Props>) => void | Change options after mount. Only the keys you pass change, so update({ user }) leaves the rest alone. |
unmount | () => void | Remove the component and its container. Calling it again does nothing. |
update()afterunmount()changes nothing. Mount again to bring the component back.update()cannot move the contact form. To changetarget, unmount and mount again.- Each call mounts a new copy. Two support widgets, or two feedback panels, share one open state and open together.
The target option
target says where the contact form goes. It is a CSS selector or an element.
import { mountMedianContactForm } from "@mediansh/widget";
mountMedianContactForm({
target: "#contact",
apiKey: "median_pk_...",
fields: [{ name: "name", label: "Name", required: true }],
});- The selector is looked up once, when you call the function. The element must exist by then.
- The form renders into its own container inside the target. Anything else in the target stays.
- If nothing matches, nothing is mounted, the console says so, and the returned instance does nothing.
- An invalid selector throws a
SyntaxErrorfrommountMedianContactForm. Nothing is logged.
Console messages
Warnings print only in development builds, where your bundler sets process.env.NODE_ENV to "development". Errors always print.
| Message | Cause | Prints |
|---|---|---|
Median: the widget was asked to open, but no <MedianSupport> is on the page. Mount it once, near the root of your app. | open() or toggle() ran and no support panel had mounted a second later | Development |
Median: the feedback panel was asked to open, but no <MedianFeedback> is on the page. Mount it once, near the root of your app. | The same, for the feedback panel | Development |
Median: more than one <MedianSupport> is mounted. They share one open state and open together. Mount one, once, near the root of your app. | Two support panels. A widget and a modal say <MedianSupport> and <MedianSupportModal> are both mounted. <MedianFeedback> has the same warning | Development |
Median: data was attached, but no <MedianSupport>, <MedianFeedback> or contact form is on the page to send it. It goes with the next message, note, sendMedianContact() or reportError() call. | attach() with nothing mounted that sends it | Development |
Median: attach() needs a label and a value, and nothing was queued. Pass a short label naming the data, and the data itself. | attach() with an empty label or value | Development |
Median: update() was called after unmount(), so nothing changed. Mount again with mountMedianSupport() if the widget should come back. | update() on an unmounted instance. The function name matches the one you called | Development |
Median: mountMedianSupport() was called where there is no document, so nothing was mounted. Call it in the browser, after the page exists. | A mount function ran on the server | Always |
Median: mountMedianContactForm() could not find an element matching "#contact", so nothing was mounted. Pass a selector that matches something on the page, or the element itself, once it exists. | target matched nothing | Always |