Skip to content

Dialog

WxDialog is a window over the page: a heading with its actions, a body that scrolls, a row of buttons under it. Use it when the page has to wait — a form to fill in, a choice to make, a record to edit before anything else happens.

A form in a dialog · the page is called «About the company»
Dragged by the heading, resized by the corner, remembered
A sidebar beside the body · width in per cent
More than fits on the screen

Usage

vue
<template>
  <wx-dialog title="Edit page" :width="520">
    <template #trigger>
      <wx-button type="primary">Edit</wx-button>
    </template>

    <template #extra>
      <wx-badge type="warning">Draft</wx-badge>
    </template>

    <wx-form>
      <wx-form-item label="Title">
        <wx-input v-model="draft" />
      </wx-form-item>
    </wx-form>

    <template #footer="{ close }">
      <wx-button @click="close">Cancel</wx-button>
      <wx-button type="primary" @click="save">Save</wx-button>
    </template>
  </wx-dialog>
</template>

The trigger slot must hold exactly one element — the dialog takes over its click and its aria-expanded. Anything already a button works; nothing of ours is wrapped around it.

Leave the slot out and drive the dialog from outside with v-model:open, which is what you want when opening it also has to prepare something:

vue
<script setup>
const open = ref(false)

watch(open, (value) => {
  if (value) draft.value = page.title
})
</script>

<template>
  <wx-button @click="open = true">Edit</wx-button>
  <wx-dialog v-model:open="open" title="Edit page">…</wx-dialog>
</template>

The three parts

SlotWhat goes in it
titleThe heading. title as a prop does the same for plain text
extraNext to the heading — badges, or actions that belong to the whole panel
defaultThe body. The one part that scrolls
sidebarA column beside the body; its presence is what splits the body in two
footerA row under the body — where the Save and Cancel go

The heading and the footer stay where they are however long the content is: only the body scrolls. With a sidebar, the two columns scroll on their own instead.

Size

width and height take a number, which is pixels, or any CSS length — 520, '60%', '40rem'. Left without a height the panel grows with its content and stops at the height of the screen.

vue
<wx-dialog title="Settings" width="70%" height="60%" />

More than fits on the screen

scroll decides what gives way when the content is longer than the screen:

  • 'body' (the default) keeps the panel inside the screen. The heading and the footer stay where they are and the body between them scrolls — the right shape for a form, where the Save is always one glance away.
  • 'panel' lets the panel grow as tall as its content and scrolls the whole of it inside the screen. The heading scrolls away with everything else — the right shape for a long list picked from, or a document read through.
vue
<wx-dialog title="Pick a brand" scroll="panel" :width="440">…</wx-dialog>

In panel mode sticky-footer decides where the buttons end up. Left on, the footer rests against the bottom of the screen while the list scrolls behind it; turned off, it sits at the end of the content and is reached by scrolling to it:

vue
<wx-dialog scroll="panel" :sticky-footer="false" />

A panel taller than the screen has nowhere to be dragged to and nothing to be stretched into, so draggable and resizable are ignored while scroll="panel" — no need to unpick them when a dialog turns out to be the long kind.

Moving and resizing

draggable lets the panel be moved by its heading; resizable adds a grip in the bottom-right corner. Both are pointer-only — a finger dragging a heading is a finger not scrolling — and on a small screen the panel fills it anyway.

persist gives the size and position a key in localStorage, so the panel opens where it was last left:

vue
<wx-dialog title="Media" draggable resizable persist="media" />

Without persist, a dialog forgets where it was dragged as soon as it closes. reset() on the component puts it back in the middle at its declared size and clears what was stored.

On a phone

Under 640px wide the panel takes the width of the screen, the paddings of the heading, body and footer tighten so that more of the content fits, a remembered size and position are ignored rather than reopening the dialog half off the screen, and a sidebar stacks above the body instead of standing beside it.

Those paddings are custom properties — --wx-dialog-pad-x, --wx-dialog-pad-y and --wx-dialog-body-pad — so a panel that wants roomier or tighter chrome can set them itself.

Focus and the page behind it

The panel takes focus when it opens and gives it back to the trigger when it closes, focus is trapped inside it, and the page behind it stops scrolling. :modal="false" lifts all three — reach for it only when the page is meant to stay usable, and for that WxDrawer is usually the better answer.

close-on-overlay and close-on-escape govern the two ways out that are not the × or a footer button; turn them off for a form where a stray click would lose what was typed.

Props

PropTypeDefaultDescription
openbooleanfalseUse with v-model:open
titlestringHeading of the panel
widthnumber | string520A number means pixels
heightnumber | stringGrows with the content when left out
scroll'body' | 'panel''body'What scrolls when the content is long
stickyFooterbooleantrueIn scroll="panel", pin the footer to the screen
minWidthnumber320Smallest width a resize may reach, px
minHeightnumber200Smallest height a resize may reach, px
sidebarWidthnumber | string200Width of the sidebar column
closablebooleantrueAdds a × to the heading
closeLabelstring'Close'Label of that ×, for screen readers
closeOnOverlaybooleantrueA click outside closes it
closeOnEscapebooleantrueEsc closes it
overlaybooleantrueDim the page behind the panel
modalbooleantrueTrap focus and block the page behind
draggablebooleanfalseMoved by its heading; off when scroll="panel"
resizablebooleanfalseGrip in the corner; off when scroll="panel"
persiststringKey under which the layout is remembered
ariaLabelstringAccessible name when there is no heading

Events: open, close, layout ({ width, height, x, y } — what persist would store). Slots: trigger ({ open }) — one element; default, extra, sidebar, footer (all { close }); title. Exposed: close(), reset().

Dialog or drawer

They are the same panel and share their props. A dialog sits in the middle of the screen and asks to be answered; a drawer is anchored to an edge and leaves the page behind it visible, which is what you want when the panel is about something on that page.

Released under the MIT License.