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>`;| Slot | What goes in it |
|---|---|
image | Optional image across the top of the card, edge to edge. |
heading | The title, in the header band. Names the dialog. |
subheading | A short description under the band. Describes it. |
| (default) | The body, under the description. |
footer | The 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, passlabel— 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 tolabeland warns, instead of leaving the dialog with an empty name. alertannounces it as analertdialog. 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-modalis 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 whiledismissableis 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
dismissabledefaults to on and is a boolean attribute, so markup cannot switch it off — set the property (.dismissable=${false}).- With
dismissableoff the dialog setsclosedby="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.