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 · type | Default | Description |
|---|---|---|
| Value | ||
| openboolean= false | false | Whether the window is open (v-model:open); every way out sets it false and emits close. |
| State | ||
| submittingboolean= false | false | A request is out (a save, a delete): every close is refused with a shake, and no second submit goes. |
| Behavior | ||
| title*string | The 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= false | false | Unsaved 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= true | true | false 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= false | false | The 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 need | Take |
|---|---|
| A record to edit | Crud drawer |
| A side panel beside a list | Drawer, to read or set at the side of the screen |
| A small panel of controls under a button | Popover, with the page live |
| A yes-or-no question | Confirm, built on this window |
| The outcome of an action | A toast from useToast in Toaster |