name: modals description: Dynamic modal system using useDynamicModalStore. Use when opening/closing modals, implementing Dialog or Sheet overlays, handling nested modals, or managing z-index for modal layering. user-invocable: false
Modal System Guide
The application uses a dynamic modal system with useDynamicModalStore that supports unlimited modals with automatic z-index management.
Key Features
- Unlimited Modals: Open as many sheets and dialogs as needed simultaneously
- Automatic Z-Index Management: Each modal automatically gets the correct z-index based on open order (FIFO)
- Custom IDs: Use custom IDs for easy modal management or let the system auto-generate them
- Type-Aware Rendering: Sheet vs Dialog components rendered correctly based on type
- No Z-Index Conflicts: Modals layer correctly - later modals appear on top
Core Components
useDynamicModalStore: Zustand store with Map-based architecture (/src/store/useDynamicModalStore.ts)DynamicModalProvider: Dynamic modal renderersheet.tsxanddialog.tsx: shadcn/ui components with z-index support (/src/components/ui/)
Basic Usage
Opening a Modal
import { useDynamicModalStore } from '@/store/useDynamicModalStore'
const MyComponent = () => {
const { openModal, closeModal } = useDynamicModalStore()
const handleOpenModal = () => {
// Option A: Auto-generated ID
const modalId = openModal('dialog', {
component: MyModalContent,
props: {
title: 'Modal Title',
description: 'Modal description',
size: 'lg', // 'sm' | 'md' | 'lg' | 'xl' | 'full'
someData: 'example'
},
onSubmit: (data) => {
console.log('Modal submitted:', data)
closeModal(modalId)
},
onClose: () => {
console.log('Modal closed')
}
})
}
return <Button onClick={handleOpenModal}>Open Modal</Button>
}
Modal Component Pattern
interface MyModalContentProps {
title?: string
description?: string
someData?: string
onSubmit?: (data: any) => void
onClose?: () => void
parentTriggerFn?: (...args: any[]) => void
}
const MyModalContent: React.FC<MyModalContentProps> = ({
someData,
onSubmit,
onClose
}) => {
const handleSubmit = (formData: any) => {
// Process form data
onSubmit?.(formData)
}
return (
<div className="space-y-4">
{/* Modal content */}
<Button onClick={() => handleSubmit(data)}>Submit</Button>
<Button variant="outline" onClick={onClose}>Cancel</Button>
</div>
)
}
Custom Modal IDs (Recommended)
For reusable or identifiable modals, use custom IDs:
const { openModal, closeModal } = useDynamicModalStore()
// Open with custom ID
openModal('dialog', {
id: 'user-edit-modal',
component: UserEditForm,
props: { title: 'Edit User', userId: 123 },
})
// Close by custom ID
closeModal('user-edit-modal')
Benefits of custom IDs:
- Easy to reference modals from anywhere in the app
- Prevent duplicate modals (opening same ID twice won't create a duplicate)
- Cleaner debugging and state management
- Better for testing
Sheet vs Dialog Usage
Dialog
Use for:
- Confirmations and alerts
- Simple forms requiring user attention
- Primary actions that block workflow
- Centered modal content
openModal('dialog', {
id: 'delete-confirmation',
component: DeleteConfirmation,
props: {
title: 'Confirm Deletion',
description: 'This action cannot be undone',
size: 'sm',
},
})
Sheet
Use for:
- Filters and advanced search
- Multi-step forms
- Detailed views and information panels
- Secondary workflows that don't interrupt main flow
openModal('sheet', {
id: 'system-filters',
component: SystemFilters,
props: {
title: 'Filter Systems',
side: 'left', // or 'right', 'top', 'bottom'
},
})
Size Options
Dialog sizes:
sm- Small (max-w-sm) - Confirmations, simple alertsmd- Medium (max-w-md) - Default, simple formslg- Large (max-w-lg) - Complex formsxl- Extra Large (max-w-xl) - Detailed contentfull- Full Screen (max-w-full) - Tables, extensive data
Sheet sizes:
- Sheets use the
sideprop instead ofsize - Width/height is determined by content and screen size
Nested Modals
The system automatically handles nested modals with proper z-index layering:
// Open spare assignment wizard
const wizardId = openModal('dialog', {
id: 'spare-wizard',
component: SpareWizardComponent,
props: { title: 'Assign Spare Part', size: 'xl' },
})
// Z-index: 50 (overlay), 51 (content)
// Inside wizard, open filter sheet
const filterId = openModal('sheet', {
id: 'system-filters',
component: FilterComponent,
props: { title: 'Filter Systems', side: 'left' },
})
// Z-index: 52 (overlay), 53 (content) - Automatically higher!
Z-Index Calculation
Base Z-Index: 50
For each modal in order:
- Overlay: baseZIndex + (modalIndex * 2)
- Content: baseZIndex + (modalIndex * 2) + 1
Example with 3 open modals:
Modal 1: overlay=50, content=51
Modal 2: overlay=52, content=53
Modal 3: overlay=54, content=55 (top layer)
Advanced Usage
Additional Store Functions
const { openModal, closeModal, bringToFront, closeAllModals, getModalById } = useDynamicModalStore()
// Bring existing modal to front
bringToFront('my-modal-id')
// Close all modals at once
closeAllModals()
// Get modal instance by ID
const modal = getModalById('my-modal-id')
if (modal) {
console.log('Modal exists:', modal.type, modal.props)
}
Parent Trigger Functions
Pass functions to modal children for advanced interactions:
const MyComponent = () => {
const { openModal, closeModal } = useDynamicModalStore()
const handleRefresh = () => {
console.log('Refreshing data...')
// Refresh logic
}
const handleOpenModal = () => {
const modalId = openModal('dialog', {
component: MyModalContent,
props: {
title: 'Edit User',
parentTriggerFn: handleRefresh
},
onSubmit: (data) => {
// Submit logic
closeModal(modalId)
handleRefresh() // Refresh after submit
}
})
}
return <Button onClick={handleOpenModal}>Edit</Button>
}
Modal State Management
// Check if specific modal is open
const modal = getModalById('my-modal-id')
const isOpen = modal !== undefined
// Get all open modals
const store = useDynamicModalStore.getState()
const openModals = Array.from(store.modals.values())
console.log(`${openModals.length} modals open`)
Best Practices
1. Always Provide Title
// Good - accessible and clear
openModal('dialog', {
component: MyContent,
props: {
title: 'Edit User Profile',
description: 'Update your personal information',
},
})
// Bad - no title
openModal('dialog', {
component: MyContent,
props: {},
})
2. Handle Both onSubmit and onClose
// Good - handles all user actions
const modalId = openModal('dialog', {
component: EditForm,
props: {
/* ... */
},
onSubmit: data => {
saveData(data)
closeModal(modalId)
},
onClose: () => {
// Cleanup if needed
console.log('Modal closed without submit')
},
})
3. Use Appropriate Size
// Good - size matches content
openModal('dialog', {
component: SimpleConfirmation,
props: { title: 'Delete?', size: 'sm' },
})
openModal('dialog', {
component: ComplexForm,
props: { title: 'Create Order', size: 'xl' },
})
4. Use Custom IDs for Important Modals
// Good - easy to reference
openModal('sheet', {
id: 'global-search',
component: GlobalSearch,
props: {
/* ... */
},
})
// Later, from anywhere:
closeModal('global-search')
5. Clean Up in onClose
// Good - cleans up resources
openModal('dialog', {
component: VideoPlayer,
props: {
/* ... */
},
onClose: () => {
stopVideo()
clearCache()
unsubscribeFromUpdates()
},
})
TypeScript Types
type ModalType = 'dialog' | 'sheet'
interface ModalOptions<P = any> {
id?: string
component: React.ComponentType<P>
props: P
onSubmit?: (...args: any[]) => void
onClose?: () => void
}
interface ModalInstance<P = any> {
id: string
type: ModalType
component: React.ComponentType<P>
props: P
onSubmit?: (...args: any[]) => void
onClose?: () => void
zIndex: number
}
interface DynamicModalStore {
modals: Map<string, ModalInstance>
openModal: <P>(type: ModalType, options: ModalOptions<P>) => string
closeModal: (id: string) => void
closeAllModals: () => void
bringToFront: (id: string) => void
getModalById: (id: string) => ModalInstance | undefined
}
Additional Resources
- For real-world examples, see examples.md