Contact form
Props and behavior for MedianContactForm, plus useMedianContact and sendMedianContact for a form of your own.
import { MedianContactForm } from "@mediansh/widget";
<MedianContactForm apiKey="median_pk_..." />;The form sends the same message the support widget would. The agent answers first, and your team can take over in the inbox. Mounted alongside MedianSupport, the two share a visitor, so a person who writes here and later opens the widget finds the same thread.
Props
| Name | Type | Description |
|---|---|---|
apiKey | string | The browser-safe median_pk_ id, derived from MEDIAN_KEY with publicKeyFromMedianKey. Required unless identity is set. |
identity | string | Path to a route built with medianIdentity. The same prop MedianSupport takes. The form shows but stays switched off until the route answers. See Identity. |
fields | MedianContactField[] | Fields to ask for besides the email and the message. |
user | MedianUser | The signed in user, if any. Their email and name fill empty boxes. What the visitor types wins. |
telemetry | boolean | Send browser context with the message: time zone, language, browser, OS, device, screen, and the page it was sent from. Default: true. |
diagnostics | boolean | { errors?: boolean; network?: boolean; console?: boolean } | Attach uncaught errors, and optionally failed requests and console.error calls, to the message. Default: false. |
appState | Record<string, string | number | boolean> | Facts about your app to send with the message. Read when the form is sent. Sent only when diagnostics is on. |
title | string | Default: "Contact us". |
description | string | Default: "We'll reply by email.". |
onSent | () => void | Called after the message is delivered, before the thank-you shows. If it throws, the error goes to the console and the thank-you still shows. |
MedianUser, telemetry, diagnostics and appState work as they do on MedianSupport. See Context and diagnostics.
Fields
Email and message are always on the form. Add the rest with fields:
<MedianContactForm
apiKey="median_pk_..."
fields={[
{ name: "name", label: "Name", required: true },
{ name: "company", label: "Company" },
{
name: "topic",
label: "Topic",
type: "select",
options: ["Billing", "A bug", "Something else"],
},
]}
/>;| Name | Type | Description |
|---|---|---|
name | string | The field's key. Three names are reserved: email, message and name. |
label | string | Shown above the box, and before the answer in the message. Made from name when left out. "order_number" and "orderNumber" both become "Order number". |
type | "text" | "textarea" | "select" | One line, a paragraph, or one choice from a list. Ignored on email and message. Default: "text". |
options | string[] | The choices for a select. A select with no options is shown as a text box. |
placeholder | string | Shown in an empty box. On a select it labels the empty choice, "Choose one" by default. |
required | boolean | Must be filled in. Fields that are not required show "Optional" beside the label. Email and message are always required. Default: false. |
| Rule | What happens |
|---|---|
| Order | Fields show in the order given. Email goes first, or right after a name field. Message goes last. Name either one in fields to place it yourself |
name: "email" or name: "message" | Changes that field's label and placeholder. Nothing else |
name: "name" | The answer goes on the customer record, not in the message |
| Any other name | The answer goes above the message as its own line, such as Topic: Billing |
| An empty optional field | Left out of the message |
| Two fields with one name | Only the first is shown |
| Built-in field | Label | Placeholder |
|---|---|---|
| you@example.com | ||
| Message | Message | How can we help? |
Validation
The form checks its fields when Send message is pressed, not while the visitor types. Each problem shows under its field, and the first one takes focus. A message clears as soon as the visitor edits that field.
| Field | Message |
|---|---|
| Email, empty | Enter your email. |
Email, not shaped like name@domain.tld | Enter a valid email address. |
| Message, empty | Write a message. |
| A required field, empty | Required. |
Enter in a one line field sends the form. Cmd+Enter or Ctrl+Enter sends from a paragraph field.
After sending
- Send message shows a spinner while the message is on its way.
- On success the card shows Message sent, a line such as "Replies go to ada@example.com." with the address given, and a Send another button. The card keeps the form's height.
- Send another brings the form back with the email and name filled in and the other fields empty.
- On failure the reason shows above the button and every answer stays.
- The email is saved in the browser.
MedianSupporton the same site will not ask for it again, and the form fills it in next time.
The form fills the width of its container and has no width of its own.
A "Powered by Median" line sits beside the button and under the thank-you. Hide it with Show Median branding in Settings → Billing, on a plan that allows it. See Plans.
What your team sees
The form's answers become one message:
Company: Acme
Topic: Billing
We were charged twice for May. Order 4821.- If this browser has an open widget conversation, the message joins it. Otherwise it starts a new Live chat conversation in the inbox. A resolved conversation is never reopened.
- The email and the name go on the customer record. Typed values win over
user. - With
telemetryon, the page URL and browser context come with it. So do diagnostics and data queued withattach(), when you use them. - The page URL is kept only when the message starts a new conversation.
- The agent and your team reply in the thread. The visitor sees the replies when they open
MedianSupportin the same browser.
:::warning Replies are copied to the visitor's email only when email support is on for your workspace. See Email. :::
The copy goes out 2 minutes after a reply the visitor has not seen. If they later open the support panel on your site, the panel decides instead, the same as for any widget visitor.
Limits
| What | Limit |
|---|---|
| Whole message, field lines included | 4,000 characters. Longer fails with "Messages have to be 4000 characters or fewer." |
| One line field, including email and name | 200 characters |
| Paragraph field | 4,000 characters |
| Email on the customer record | Trimmed and cut at 320 characters |
| Name on the customer record | Trimmed and cut at 80 characters |
| New conversations and sends | Rate limited. Over the limit fails with "Too many requests. Wait a moment and try again." See Rate limits |
Your own form
useMedianContact is the same send without the markup:
import { useMedianContact } from "@mediansh/widget";
import { useState } from "react";
function ContactPage() {
const contact = useMedianContact({ apiKey: "median_pk_..." });
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
return (
<form
onSubmit={async (event) => {
event.preventDefault();
await contact.send({ email, message }).catch(() => {});
}}
>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<textarea value={message} onChange={(e) => setMessage(e.target.value)} />
{contact.error && <p>{contact.error}</p>}
<button disabled={!contact.isReady || contact.isSending}>Send</button>
</form>
);
}It takes apiKey or identity, plus user, telemetry, diagnostics and appState, as the component does. It needs no provider and no other component on the page.
| Name | Type | Description |
|---|---|---|
send | (message: MedianContactMessage) => Promise<void> | Resolves once the message is delivered. Rejects with an Error whose message is fit to show, and puts the same text in error. |
isSending | boolean | A send is in flight. |
error | string | null | Why the last send failed. Cleared when the next send starts. |
isReady | boolean | Whether a send can go. False on the server, and until an identity route has answered with the key. |
user | MedianUser | undefined | What the page knows about the visitor. That is the user you passed or the identity route returned, plus the email this browser already gave. Use it to fill your boxes. |
showBranding | boolean | Whether to show a "Powered by Median" line. False only when your plan allows hiding it and Show Median branding is off. |
send does not check the email or the message. Validate in your form. It fails before sending with these messages:
| Message | Cause |
|---|---|
| This form is not connected yet. Try again in a moment. | No key yet. The identity route has not answered |
| Still connecting. Try again in a moment. | No browser session yet |
The message
send and sendMedianContact take a MedianContactMessage:
| Name | Type | Description |
|---|---|---|
email | string | Where replies go. Saved on the customer record. |
message | string | What the visitor wrote. |
name | string | Saved on the customer record beside the email. |
fields | Record<string, string> | Other answers, as label to answer. Each goes above the message as its own line. { Topic: "Billing" } becomes "Topic: Billing". Empty answers are left out. |
Without React
sendMedianContact is the same send as a plain function:
import { sendMedianContact } from "@mediansh/widget";
form.addEventListener("submit", async (event) => {
event.preventDefault();
try {
await sendMedianContact(
{ apiKey: "median_pk_..." },
{
email: email.value,
message: message.value,
fields: { Topic: topic.value },
},
);
} catch (error) {
notice.textContent = error.message;
}
});| Name | Type | Description |
|---|---|---|
apiKey | string | The browser-safe median_pk_ id. |
user | MedianUser | The signed in user, if any. The email and name in the message win over it. |
telemetry | boolean | Send browser context with the message. Default: true. |
- It takes no
identity,diagnosticsorappState. Use the hook for those. - It does not check the email or the message.
- Called on the server, it throws "sendMedianContact() was called where there is no browser, so nothing was sent. Call it from the page, after it has loaded."
Both the hook and the function save the email in the browser after a successful send.
To render the ready-made form on a page without React, use mountMedianContactForm with a target. See Without React.
Troubleshooting
| You see | Cause |
|---|---|
The form stays switched off. Console: Median: <MedianContactForm> was given neither apiKey nor identity, so it cannot send. From the hook, it names useMedianContact() | Pass apiKey, or identity with the path of your identity route |
The form stays switched off. Console: Median: could not read the identity route. | The identity route failed and no apiKey was passed. See Identity |
Console: Median: the apiKey passed to <MedianContactForm> is "...", which does not look like the browser-safe id from publicKeyFromMedianKey(MEDIAN_KEY). From the hook, it names useMedianContact() | Derive apiKey from MEDIAN_KEY with publicKeyFromMedianKey. It starts with median_pk_ |
| "That API key does not match any organization." above the button | The key was revoked, or is not a Median key. Derive it from your current MEDIAN_KEY |
Console: Median: in the fields passed to <MedianContactForm>, ... | A field has no name, shares a name with another, or is a select with no options. The message says which |
| A reason above the button | The send failed. The answers stay, so the visitor can press Send message again |