Skip to content
Median
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

PropType
isOpen?boolean

Whether the panel is open, however it was opened.

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

Typenumber
open?() => void

Open the panel.

Type() => void
close?() => void

Close the panel.

Type() => void
toggle?() => void

Open the panel if it is closed, close it if it is open.

Type() => void
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.

Type(label: string, value: unknown) => void
reportError?(error: unknown, options?: MedianErrorReportOptions) => Promise<MedianErrorReportOutcome>

File the crash your error screen is showing. See Crash reports.

Type(error: unknown, options?: MedianErrorReportOptions) => Promise<MedianErrorReportOutcome>
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.

Type() => void
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.
  • MedianSupport and MedianSupportModal share 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() or toggle() from the hook or medianSupport.
  • The corner panel opens where the button would be. MedianSupportModal still opens in the middle of the screen.
  • There is no count or reply card to show new replies. Put unreadCount on your own button, as in the example at the top. The count in the tab title still works. Turn it off with titleCount={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 open is passed, the panel shows exactly open. Everything in the table calls onOpenChange and changes nothing until you update open.
  • Without the open prop, the panel changes directly and onOpenChange still hears about it.
  • isOpen from the hook follows the open prop.

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>;
}
PropType
isOpen?boolean

Whether the panel is open, however it was opened.

Typeboolean
open?() => void

Open the panel.

Type() => void
close?() => void

Close the panel.

Type() => void
toggle?() => void

Open the panel if it is closed, close it if it is open.

Type() => void

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.

PropType
update?(options: Partial<Props>) => void

Change options after mount. Only the keys you pass change, so update({ user }) leaves the rest alone.

Type(options: Partial<Props>) => void
unmount?() => void

Remove the component and its container. Calling it again does nothing.

Type() => void
  • update() after unmount() changes nothing. Mount again to bring the component back.
  • update() cannot move the contact form. To change target, 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 SyntaxError from mountMedianContactForm. 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

Was this page helpful?