Mediquo documentation home

Dialog

A modal window for a task or decision that needs the patient's full attention.

mq-dialog is the SDK's modal, moved into @mediquo/ui and restyled to match the patient portal's dialog: a tinted header band with the title and the close button, the body, and the actions under a separator. It ships as a custom element and as a React component, and @mediquo/elements/ui/mq-dialog re-exports it, so SDK screens and the portal share one piece.

Install

// Custom element — the import registers <mq-dialog>
import "@mediquo/ui/components/dialog";
// React
import { Dialog } from "@mediquo/ui/react/dialog";

Usage

The owner holds open and follows mq-close, which fires when the user closes it with the close button, Escape or a click outside. The event is cancelable: call preventDefault() to keep the dialog open — for unsaved changes, or while a request is in flight.

html`<mq-dialog
  .open=${this.open}
  .closeLabel=${msg("Cerrar")}
  @mq-close=${() => (this.open = false)}
>
  <img slot="image" src="…" alt="" />
  <h2 slot="heading">Confirmar cancelación</h2>
  <p slot="subheading">¿Seguro que quieres cancelar esta cita?</p>
  …
  <mq-button slot="footer" variant="outline">Volver</mq-button>
  <mq-button slot="footer" variant="primary">Confirmar</mq-button>
</mq-dialog>`;
SlotWhat goes in it
imageOptional image across the top of the card, edge to edge.
headingThe title, in the header band. Names the dialog.
subheadingA short description under the band. Describes it.
(default)The body, under the description.
footerThe actions, end-aligned under a separator.

Image

An image in the image slot spans the card's full width, up to --dialog-image-max-height (12rem) tall and cropped with object-fit: cover. The close button gets a round chip behind it so it stays visible on a photo. Without an image, the area takes no space at all. The header band sits under the image.

Give the image alt="" when it is decorative, which it usually is — the heading already says what the dialog is about.

Mobile and desktop

The card is as wide as the screen minus 1rem on each side, up to 25rem, and never taller than the viewport: its content scrolls inside it while the close button and the actions stay put. It respects the --mq-safe-* insets on devices with a notch. When the actions do not fit on one line they wrap.

Accessibility

  • The heading names the dialog (aria-labelledby) and the subheading describes it (aria-describedby); each is only referenced when it is there. With no heading, pass label — the component warns in the console when a dialog opens with no name at all.
  • An empty heading does not count: <h2 slot="heading"></h2> falls back to label and warns, instead of leaving the dialog with an empty name.
  • alert announces it as an alertdialog. Use it for confirmations, above all destructive ones such as cancelling an appointment.
  • The native showModal() makes the page behind it inert; Tab also wraps inside the dialog so focus never leaks into the browser chrome. Closing hands focus back to what had it before — including when the dialog is removed from the page while open. aria-modal is only set when the dialog really opened as a modal.
  • When the content is taller than the card, the scrolling area takes keyboard focus (tabindex="0", role="region", named by the heading), so it can be scrolled without a mouse — Safari does not make scrollers focusable itself.
  • The page scroll is locked while it is open, giving the scrollbar's width back as padding so the host page does not jump.
  • A click only counts as outside when it starts and ends on the underlay, so a text selection dragged out of the card does not close it.
  • The close button is named by close-label (default "Close dialog") and is disabled while dismissable is off. Pass it translated — the default is English, and a Spanish screen reader would read it in English.
  • The animations are dropped under prefers-reduced-motion: reduce.

Styling

The --dialog-* tokens are exported from @mediquo/ui/tokens (as components.dialog) and defined in tokens.css; the component falls back to the same values, so it looks right whether or not the stylesheet is loaded. Colours come from the --mq-* semantic tokens when they are loaded and from the SDK theme's --color-* otherwise, so white-label themes still apply.

mq-dialog {
  --dialog-underlay: rgb(0 0 0 / 0.5);
  --dialog-underlay-blur: 4px;
  --dialog-shadow: 0px 8px 16px 0px rgb(0 0 0 / 0.1);
  --dialog-close-hover-bg: rgb(0 0 0 / 0.05);
  --dialog-transition-duration: 200ms;
  --dialog-radius: 0.5rem;
  --dialog-header-height: 3.75rem;
  --dialog-padding-inline: 0.75rem;
  --dialog-padding-block: 1.25rem;
  --dialog-gap: 1rem;
  --dialog-actions-gap: 0.625rem;
  --dialog-max-width: 25rem;
  --dialog-image-max-height: 12rem;
}

part is exposed for dialog (the card), close, image, header, heading, body, subheading, content and footer.

Behaviour worth knowing

  • dismissable defaults to on and is a boolean attribute, so markup cannot switch it off — set the property (.dismissable=${false}).
  • With dismissable off the dialog sets closedby="none", so the browser does not treat Escape as a close request at all. Where a browser closes it anyway (older Chrome lets a repeated Escape through), the dialog opens itself again.
  • A close the browser forces while dismissable is reported as mq-close, but cannot be cancelled.