Documentation Index
Fetch the complete documentation index at: https://mintlify.com/zrclouddev-oss/saas-starter-vue/llms.txt
Use this file to discover all available pages before exploring further.
Overview
SaaS Starter Vue includes several overlay components for displaying content on top of the main interface. All components are built with reka-ui for full accessibility and keyboard navigation support.
Dialog (Modal)
Dialogs are modal overlays that require user interaction before dismissing.
Basic Dialog
<script setup lang="ts">
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
</script>
<template>
<Dialog>
<DialogTrigger as-child>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
This is a description of what this dialog is about.
</DialogDescription>
</DialogHeader>
<!-- Dialog content -->
</DialogContent>
</Dialog>
</template>
Dialogs commonly contain forms for user input:
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from '@inertiajs/vue3'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import InputError from '@/components/InputError.vue'
const open = ref(false)
const form = useForm({
name: '',
email: '',
})
const handleSubmit = () => {
form.post('/endpoint', {
onSuccess: () => {
open.value = false
form.reset()
},
})
}
</script>
<template>
<Dialog v-model:open="open">
<DialogTrigger as-child>
<Button>Create User</Button>
</DialogTrigger>
<DialogContent>
<form @submit.prevent="handleSubmit">
<DialogHeader>
<DialogTitle>Create New User</DialogTitle>
<DialogDescription>
Fill in the details below to create a new user.
</DialogDescription>
</DialogHeader>
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<Label for="name">Name</Label>
<Input id="name" v-model="form.name" />
<InputError :message="form.errors.name" />
</div>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input id="email" v-model="form.email" type="email" />
<InputError :message="form.errors.email" />
</div>
</div>
<DialogFooter>
<DialogClose as-child>
<Button variant="outline" type="button">Cancel</Button>
</DialogClose>
<Button type="submit" :disabled="form.processing">
Create
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>
Confirmation Dialog
Example from resources/js/components/DeleteUser.vue:40-111:
<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { useTemplateRef } from 'vue'
import { destroy as deleteProfile } from '@/actions/App/Http/Controllers/System/Settings/ProfileController'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import InputError from '@/components/InputError.vue'
const passwordInput = useTemplateRef('passwordInput')
</script>
<template>
<Dialog>
<DialogTrigger as-child>
<Button variant="destructive">Delete account</Button>
</DialogTrigger>
<DialogContent>
<Form
v-bind="deleteProfile()"
reset-on-success
@error="() => passwordInput?.$el?.focus()"
v-slot="{ errors, processing, reset, clearErrors }"
class="space-y-6"
>
<DialogHeader class="space-y-3">
<DialogTitle>
Are you sure you want to delete your account?
</DialogTitle>
<DialogDescription>
Once your account is deleted, all of its resources and data
will also be permanently deleted. Please enter your password
to confirm you would like to permanently delete your account.
</DialogDescription>
</DialogHeader>
<div class="grid gap-2">
<Label for="password" class="sr-only">Password</Label>
<Input
id="password"
type="password"
name="password"
ref="passwordInput"
placeholder="Password"
/>
<InputError :message="errors.password" />
</div>
<DialogFooter class="gap-2">
<DialogClose as-child>
<Button
variant="secondary"
@click="() => { clearErrors(); reset(); }"
>
Cancel
</Button>
</DialogClose>
<Button
type="submit"
variant="destructive"
:disabled="processing"
>
Delete account
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
</template>
Dialog Components
- Dialog - Root component that manages state
- DialogTrigger - Button or element that opens the dialog
- DialogContent - Container for dialog content with overlay
- DialogHeader - Header section for title and description
- DialogTitle - Main heading (required for accessibility)
- DialogDescription - Supporting text
- DialogFooter - Footer section for actions
- DialogClose - Button to close dialog
- DialogOverlay - Background overlay (included in DialogContent)
Controlled Dialog
Manage dialog state externally:
<script setup lang="ts">
import { ref } from 'vue'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
const isOpen = ref(false)
const openDialog = () => {
isOpen.value = true
}
const closeDialog = () => {
isOpen.value = false
}
</script>
<template>
<div>
<Button @click="openDialog">Open Dialog</Button>
<Dialog v-model:open="isOpen">
<DialogContent>
<!-- content -->
<Button @click="closeDialog">Close</Button>
</DialogContent>
</Dialog>
</div>
</template>
For long content, use DialogScrollContent:
<script setup lang="ts">
import {
Dialog,
DialogScrollContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
</script>
<template>
<Dialog>
<DialogScrollContent>
<DialogHeader>
<DialogTitle>Long Content</DialogTitle>
</DialogHeader>
<!-- Long scrollable content -->
<div class="space-y-4">
<!-- ... lots of content ... -->
</div>
</DialogScrollContent>
</Dialog>
</template>
Remove the X close button:
<template>
<DialogContent :show-close-button="false">
<!-- Content -->
</DialogContent>
</template>
Sheet (Slide-over)
Sheets slide in from the edge of the screen:
<script setup lang="ts">
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
</script>
<template>
<Sheet>
<SheetTrigger as-child>
<Button variant="outline">Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>
Additional information goes here.
</SheetDescription>
</SheetHeader>
<!-- Sheet content -->
</SheetContent>
</Sheet>
</template>
Sheet Sides
Sheets can slide from different sides:
<template>
<div class="flex gap-2">
<Sheet>
<SheetTrigger as-child>
<Button>Left</Button>
</SheetTrigger>
<SheetContent side="left">
<!-- content -->
</SheetContent>
</Sheet>
<Sheet>
<SheetTrigger as-child>
<Button>Right</Button>
</SheetTrigger>
<SheetContent side="right">
<!-- content (default) -->
</SheetContent>
</Sheet>
<Sheet>
<SheetTrigger as-child>
<Button>Top</Button>
</SheetTrigger>
<SheetContent side="top">
<!-- content -->
</SheetContent>
</Sheet>
<Sheet>
<SheetTrigger as-child>
<Button>Bottom</Button>
</SheetTrigger>
<SheetContent side="bottom">
<!-- content -->
</SheetContent>
</Sheet>
</div>
</template>
Popover
Non-modal overlay for displaying content:
<script setup lang="ts">
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button variant="outline">Open Popover</Button>
</PopoverTrigger>
<PopoverContent>
<div class="space-y-2">
<h4 class="font-medium">Popover Title</h4>
<p class="text-sm text-muted-foreground">
This is popover content that appears above other content.
</p>
</div>
</PopoverContent>
</Popover>
</template>
Popover Positioning
<template>
<PopoverContent side="top" align="start">
<!-- content -->
</PopoverContent>
</template>
Options:
- side:
'top' | 'right' | 'bottom' | 'left'
- align:
'start' | 'center' | 'end'
Contextual menus for actions:
<script setup lang="ts">
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
import { MoreVertical, Edit, Trash } from 'lucide-vue-next'
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<MoreVertical class="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Edit class="size-4 mr-2" />
Edit
</DropdownMenuItem>
<DropdownMenuItem class="text-destructive">
<Trash class="size-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
Dropdown with Shortcuts
<script setup lang="ts">
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger>Menu</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
New File
<DropdownMenuShortcut>⌘N</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Save
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
Right-click menu:
<script setup lang="ts">
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from '@/components/ui/context-menu'
</script>
<template>
<ContextMenu>
<ContextMenuTrigger class="border rounded-lg p-8">
Right click me
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Copy</ContextMenuItem>
<ContextMenuItem>Paste</ContextMenuItem>
<ContextMenuItem>Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</template>
Short hints on hover:
<script setup lang="ts">
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { Button } from '@/components/ui/button'
import { Info } from 'lucide-vue-next'
</script>
<template>
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon">
<Info class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>This is helpful information</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
Toast Notifications
Using vue-sonner for toast notifications:
<script setup lang="ts">
import { toast } from 'vue-sonner'
import { Button } from '@/components/ui/button'
const showToast = () => {
toast.success('Operation completed successfully!')
}
const showError = () => {
toast.error('Something went wrong!')
}
const showInfo = () => {
toast.info('Here is some information')
}
const showWarning = () => {
toast.warning('Please be careful')
}
</script>
<template>
<div class="flex gap-2">
<Button @click="showToast">Success</Button>
<Button @click="showError" variant="destructive">Error</Button>
<Button @click="showInfo" variant="outline">Info</Button>
<Button @click="showWarning" variant="secondary">Warning</Button>
</div>
</template>
Make sure to include the Sonner component in your layout:
<script setup lang="ts">
import { Sonner } from '@/components/ui/sonner'
</script>
<template>
<div>
<!-- Your app content -->
<Sonner />
</div>
</template>
Toast with Actions
import { toast } from 'vue-sonner'
toast.success('Event created', {
action: {
label: 'View',
onClick: () => console.log('View clicked'),
},
})
Accessibility
All overlay components include:
- Focus management - Focus is trapped within dialogs
- Keyboard navigation - ESC to close, Tab to navigate
- ARIA attributes - Proper roles and labels
- Screen reader support - Announcements and descriptions
- Focus restoration - Focus returns to trigger on close
Best Practices
- Dialog vs Sheet - Use dialogs for critical actions, sheets for contextual content
- Always include titles - Required for accessibility
- Limit dialog content - Keep dialogs focused and concise
- Use confirmation dialogs - For destructive actions
- Manage focus - Ensure focus moves appropriately
- Close on success - Programmatically close after successful form submission
- Toast duration - Keep messages brief and auto-dismiss
- Dropdown positioning - Ensure dropdowns don’t overflow viewport