Median

Control from code

Open, close and watch the support and feedback panels from your own code, with or without React.

Updated Oct 1, 20266 minute read
app/layout.tsx
import { MedianSupport } from "@mediansh/widget";

<MedianSupport apiKey="median_pk_..." launcher="hidden" />;
components/help-button.tsx
"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.

NameTypeDescription
isOpenbooleanWhether the panel is open, however it was opened.
unreadCountnumberReplies 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() => voidOpen the panel.
close() => voidClose the panel.
toggle() => voidOpen the panel if it is closed, close it if it is open.
attach(label: string, value: unknown) => voidQueue 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() => voidForget 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 inReact componentsAnywhere, including code with no React
isOpen and unreadCountRe-render the component when they changeRead the current value. Nothing re-renders, and there is no change event
On the server and the first client renderisOpen is false, unreadCount is 0The 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" />
launcherWhile 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 itCalls onOpenChange
The launcher buttonYes
The unread reply cardYes
The close button in the panel headerYes
EscapeYes
A press on the modal's backdropYes
Following a page link from a reply, on a phone or in the modalYes
open(), close() and toggle() from the hook or medianSupportYes
  • 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>;
}
NameTypeDescription
isOpenbooleanWhether the panel is open, however it was opened.
open() => voidOpen the panel.
close() => voidClose the panel.
toggle() => voidOpen 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 itCalls onOpenChange
The trigger you wrappedYes
The close buttonYes
EscapeYes
A press outside the panel and the triggerYes
The panel closing itself after a note is sentYes
open(), close() and toggle() from the hook or medianFeedbackYes

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.

main.ts
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.

FunctionRendersOptionsWhere it goes
mountMedianSupport(options)The support widget in the cornerEvery MedianSupport propA <div data-median-support> appended to <body>
mountMedianSupportModal(options)The support widget as a modalEvery MedianSupportModal propA <div data-median-support-modal> appended to <body>
mountMedianFeedback(options)The feedback panelEvery MedianFeedback prop except children. It opens in the bottom right corner, or next to anchorA <div data-median-feedback> appended to <body>
mountMedianContactForm(options)The contact formEvery 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.

NameTypeDescription
update(options: Partial<Props>) => voidChange options after mount. Only the keys you pass change, so update({ user }) leaves the rest alone.
unmount() => voidRemove the component and its container. Calling it again does nothing.
  • 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.

MessageCausePrints
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 laterDevelopment
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 panelDevelopment
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 warningDevelopment
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 itDevelopment
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 valueDevelopment
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 calledDevelopment
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 serverAlways
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 nothingAlways

Still need help?

    Esc