modals

star 2

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.

eli-eric By eli-eric schedule Updated 2/12/2026

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 renderer
  • sheet.tsx and dialog.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 alerts
  • md - Medium (max-w-md) - Default, simple forms
  • lg - Large (max-w-lg) - Complex forms
  • xl - Extra Large (max-w-xl) - Detailed content
  • full - Full Screen (max-w-full) - Tables, extensive data

Sheet sizes:

  • Sheets use the side prop instead of size
  • 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

Install via CLI
npx skills add https://github.com/eli-eric/ELI-panda --skill modals
Repository Details
star Stars 2
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator