Skip to content

Modal

Complete a short task in a dialog, such as filling a form or reviewing a record's details. Built on it: Confirm.

<script setup lang="ts">import { computed, ref } from 'vue'import { UiButton, UiModal, UiField, UiInput, UiSelect, UiTextarea } from 'grani' const create = ref(false) // A save with no name stops on the field: the window puts the cursor there. A save that passes// waits for the server: the window holds every close until it answers.const nameError = ref('') const saving = ref(false) const BLANK = { name: 'Acme Corp', country: 'nl', email: 'billing@acme.example', note: '' } const client = ref({ ...BLANK }) // Typed but not saved: a click beside the window or Escape shakes it instead of closing.const dirty = computed(() => JSON.stringify(client.value) !== JSON.stringify(BLANK)) function openCreate() {  client.value = { ...BLANK }  nameError.value = ''  create.value = true} async function createClient() {  nameError.value = client.value.name.trim() ? '' : 'Give the client a name'  if (nameError.value) return  saving.value = true  await new Promise((r) => setTimeout(r, 900))  saving.value = false  create.value = false} const countries = [  { value: 'nl', label: 'Netherlands' },  { value: 'de', label: 'Germany' },  { value: 'fr', label: 'France' },]</script> <template>  <UiButton variant="outline" @click="openCreate">New client</UiButton>  <UiModal    v-model:open="create"    title="New client"    :persistent="dirty"    :submitting="saving"    form    @submit="createClient"  >    <div class="flex flex-col gap-4">      <div class="grid grid-cols-2 gap-4">        <UiField label="Name" required :error="nameError"          ><UiInput v-model="client.name" @input="nameError = ''"        /></UiField>        <UiField label="Country">          <UiSelect v-model="client.country" :items="countries" class="w-full" />        </UiField>      </div>      <UiField label="Email"><UiInput v-model="client.email" type="email" /></UiField>      <UiField label="Note"><UiTextarea v-model="client.note" /></UiField>    </div>    <template #footer>      <UiButton variant="secondary" :disabled="saving" @click="create = false">Cancel</UiButton>      <UiButton type="submit" :loading="saving">Create client</UiButton>    </template>  </UiModal></template>

Also called dialog, modal dialog, window.

Usage

<UiModal :title="title">…</UiModal>

With GraniResolver in the Vite config, a template needs no import: set it up once.

API

Name · typeDefaultDescription
Value
openboolean= falsefalseWhether the window is open (v-model:open); every way out sets it false and emits close.
State
submittingboolean= falsefalseA request is out (a save, a delete): every close is refused with a shake, and no second submit goes.
Behavior
title*stringThe window's name, read first by a screen reader; required, a dialog without one is mute.
descriptionstring= """"One line under the title, read right after it.
persistentboolean= falsefalseUnsaved input: the backdrop and Escape are refused with a shake; the cross still closes.
roledialog | alertdialog= "dialog""dialog"alertdialog for a question that needs an answer: the backdrop does not close it, focus starts on the footer's first button.
paddedboolean= truetruefalse for rows edge to edge (a detail of key and value): the body keeps no padding of its own.
Look
sizesm | md= "md""md"Width: sm for a question or a short form, md for a form with a few rows.
alignstart | center= "start""start"center for a result after an action: a glyph over a centered title, no lines between the parts.
Form and access
formboolean= falsefalseThe body and the footer are a form: its type="submit" button, Enter in a field and Cmd+Enter from a text box send submit. Off, a question or a detail keeps its buttons plain.

Attributes are placed by the component itself: its description says where.

When to use another component

When you needTake
A record to editCrud drawer
A side panel beside a listDrawer, to read or set at the side of the screen
A small panel of controls under a buttonPopover, with the page live
A yes-or-no questionConfirm, built on this window
The outcome of an actionA toast from useToast in Toaster