modal-dialog-builder
Build accessible modal dialogs with focus management, keyboard navigation, backdrop, animations. React, Vue 3, Headless UI, Radix UI. Keywords - modal, dialog, popup, overlay, accessible modal, focus trap, keyboard navigation, headless ui
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: modal-dialog-builder
description: Build accessible modal dialogs with focus management, keyboard navigation, backdrop, animations. React, Vue 3, Headless UI, Radix UI. Keywords - modal, dialog, popup, overlay, accessible modal, focus trap, keyboard navigation, headless ui
---
# Modal/Dialog Builder
## When to Use This Skill
Activate when:
- Creating modal dialogs for forms, confirmations, alerts
- Need accessible modals (focus management, ARIA, keyboard)
- Building dialogs with Headless UI or Radix UI
- Want animated transitions and backdrop
- Implementing nested modals or multi-step wizards
## What This Skill Does
Creates modal dialogs with:
- Accessibility (ARIA roles, focus trap, keyboard navigation)
- Focus management (trap focus inside modal)
- Keyboard navigation (ESC to close, Tab trap)
- Backdrop click to close
- Smooth animations (enter/exit)
- Multiple sizes and variants
- Nested modals support
## Supported Technologies
**React**:
- Headless UI (recommended - accessible by default)
- Radix UI Dialog
- React Modal
- Custom implementation
**Vue 3**:
- Headless UI Vue
- Custom implementation with Composition API
**Styling**:
- Tailwind CSS
- CSS Modules
- Styled Components
## Example: Headless UI Modal (React)
```tsx
// components/Modal.tsx
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, ReactNode } from 'react';
import { XMarkIcon } from '@heroicons/react/24/outline';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
showCloseButton?: boolean;
}
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
};
export function Modal({
isOpen,
onClose,
title,
children,
size = 'md',
showCloseButton = true,
}: ModalProps) {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
{/* Backdrop */}
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/25 backdrop-blur-sm" />
</Transition.Child>
{/* Modal container */}
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel
className={`w-full ${sizeClasses[size]} transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all`}
>
{/* Header */}
<div className="flex items-start justify-between">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
{title}
</Dialog.Title>
{showCloseButton && (
<button
type="button"
className="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={onClose}
aria-label="Close modal"
>
<XMarkIcon className="h-6 w-6" />
</button>
)}
</div>
{/* Content */}
<div className="mt-4">{children}</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
}
// Usage
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Edit User"
size="lg"
>
<p className="text-sm text-gray-500">
Make changes to your profile here.
</p>
<form className="mt-4 space-y-4">
<input
type="text"
placeholder="Name"
className="w-full rounded-md border px-3 py-2"
/>
<input
type="email"
placeholder="Email"
className="w-full rounded-md border px-3 py-2"
/>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setIsOpen(false)}
className="rounded-md bg-gray-200 px-4 py-2 hover:bg-gray-300"
>
Cancel
</button>
<button
type="submit"
className="rounded-md bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700"
>
Save
</button>
</div>
</form>
</Modal>
</>
);
}
```
## Example: Confirmation Dialog (React)
```tsx
// components/ConfirmDialog.tsx
import { Modal } from './Modal';
interface ConfirmDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
variant?: 'danger' | 'warning' | 'info';
}
export function ConfirmDialog({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = 'Confirm',
cancelText = 'Cancel',
variant = 'info',
}: ConfirmDialogProps) {
const variantStyles = {
danger: 'bg-red-600 hover:bg-red-700',
warning: 'bg-yellow-600 hover:bg-yellow-700',
info: 'bg-indigo-600 hover:bg-indigo-700',
};
const handleConfirm = () => {
onConfirm();
onClose();
};
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size="sm">
<p className="text-sm text-gray-500">{message}</p>
<div className="mt-6 flex justify-end gap-3">
<button
onClick={onClose}
className="rounded-md bg-gray-200 px-4 py-2 text-sm font-medium hover:bg-gray-300"
>
{cancelText}
</button>
<button
onClick={handleConfirm}
className={`rounded-md px-4 py-2 text-sm font-medium text-white ${variantStyles[variant]}`}
>
{confirmText}
</button>
</div>
</Modal>
);
}
// Usage
<ConfirmDialog
isOpen={isDeleteOpen}
onClose={() => setIsDeleteOpen(false)}
onConfirm={() => deleteUser(userId)}
title="Delete User"
message="Are you sure you want to delete this user? This action cannot be undone."
confirmText="Delete"
variant="danger"
/>
```
## Example: Vue 3 Modal (Headless UI)
```vue
<!-- components/Modal.vue -->
<script setup lang="ts">
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
import { XMarkIcon } from '@heroicons/vue/24/outline';
interface Props {
modelValue: boolean;
title: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
showCloseButton?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
size: 'md',
showCloseButton: true,
});
const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
};
const close = () => {
emit('update:modelValue', false);
};
</script>
<template>
<TransitionRoot appear :show="modelValue" as="template">
<Dialog as="div" @close="close" class="relative z-50">
<!-- Backdrop -->
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black/25 backdrop-blur-sm" />
</TransitionChild>
<!-- Modal container -->
<div class="fixed inset-0 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel
:class="[
'w-full transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all',
sizeClasses[size],
]"
>
<!-- Header -->
<div class="flex items-start justify-between">
<DialogTitle
as="h3"
class="text-lg font-medium leading-6 text-gray-900"
>
{{ title }}
</DialogTitle>
<button
v-if="showCloseButton"
type="button"
class="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
@click="close"
aria-label="Close modal"
>
<XMarkIcon class="h-6 w-6" />
</button>
</div>
<!-- Content -->
<div class="mt-4">
<slot />
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';
import Modal from './components/Modal.vue';
const isOpen = ref(false);
</script>
<template>
<button @click="isOpen = true">Open Modal</button>
<Modal v-model="isOpen" title="Edit User" size="lg">
<p class="text-sm text-gray-500">
Make changes to your profile here.
</p>
<form class="mt-4 space-y-4" @submit.prevent="handleSubmit">
<input
type="text"
placeholder="Name"
class="w-full rounded-md border px-3 py-2"
/>
<input
type="email"
placeholder="Email"
class="w-full rounded-md border px-3 py-2"
/>
<div class="flex justify-end gap-2">
<button
type="button"
@click="isOpen = false"
class="rounded-md bg-gray-200 px-4 py-2 hover:bg-gray-300"
>
Cancel
</button>
<button
type="submit"
class="rounded-md bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700"
>
Save
</button>
</div>
</form>
</Modal>
</template>
```
## Accessibility Checklist
✅ **Keyboard navigation**:
- ESC to close
- Tab trap (focus stays inside modal)
- Focus first interactive element on open
- Return focus to trigger on close
✅ **ARIA attributes**:
- `role="dialog"` or use `<dialog>` element
- `aria-modal="true"`
- `aria-labelledby` (title ID)
- `aria-describedby` (content ID)
✅ **Visual**:
- Visible focus indicators
- Sufficient color contrast
- Backdrop to dim background
## Best Practices
1. **Use Headless UI** - Accessible by default (recommended)
2. **Focus trap** - Prevent Tab from leaving modal
3. **ESC to close** - Standard keyboard interaction
4. **Backdrop click** - Close on outside click (optional)
5. **Return focus** - Return to trigger element on close
6. **Scroll lock** - Prevent body scroll when modal open
7. **Animations** - Smooth enter/exit transitions
8. **Nested modals** - Support multiple layers (z-index management)
## Common Modal Variants
**1. Alert/Notification**:
- Single "OK" button
- Auto-close after timeout
**2. Confirmation**:
- "Cancel" and "Confirm" buttons
- Variants: danger, warning, info
**3. Form**:
- Input fields
- "Cancel" and "Submit" buttons
- Validation
**4. Full-screen**:
- Large content (image viewer, video)
- Close button in corner
## Integration with Other Skills
- **form-builder-with-validation** - Forms inside modals
- **loading-states-handler** - Loading states during submission
- **toast-notification-system** - Success/error toasts after modal actions
---
**Version**: 1.0.0
**Category**: Frontend Extended
**Complexity**: Medium