Modals
Overview
The Modals library provides a comprehensive set of modal components built on top of Chakra UI's Modal system. These components offer different modal variants for various use cases, from simple notifications to complex form dialogs and confirmation actions.
Modal Components
Basic Modals
- Modal - Basic modal wrapper
- WPModal - Standard modal with consistent styling
- ActionModal - Feature-rich modal for actions and forms
Confirmation Modals
- ActionConfirmationModal - Confirmation dialog with actions
- DeleteConfirmationModal - Specialized delete confirmation
- DeleteModalWithAlert - Delete modal with warning icons
- ConfirmationPopover - Lightweight confirmation popover
Response Modals
- SuccessResponseModal - Success state modal
- SuccessModalBody - Success message body component
- SuccessComponent - Reusable success component
Specialized Modals
- ExportModal - Data export configuration modal
- Terms - Terms and conditions modal
Related Components
- ActionDrawer - Side drawer alternative to modals
When to Use
WPModal
- Use for simple content display
- When you need basic modal functionality
- For quick information dialogs
ActionModal
- Use for complex forms and actions
- When you need customizable headers, footers, and success states
- For multi-step workflows
ActionConfirmationModal
- Use for action confirmations
- When you need to prevent accidental actions
- For yes/no decision dialogs
DeleteConfirmationModal
- Use specifically for delete operations
- When you need to confirm destructive actions
- For data removal confirmations
SuccessResponseModal
- Use to show success feedback
- When you need to confirm completed actions
- For positive user feedback
ExportModal
- Use for data export functionality
- When you need to configure export parameters
- For file format and column selection
Common Props
Standard Modal Props
All modal components share these common props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| isOpen | boolean | ✓ | - | Whether modal is open |
| onClose | function | ✓ | - | Close handler function |
| children | ReactNode | - | - | Modal content |
| heading | string/ReactNode | - | - | Modal title |
| size | string | - | 'md' | Modal size |
| isCentered | boolean | - | false | Center modal vertically |
Action Modal Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| footer | ReactNode | - | - | Custom footer content |
| isLoading | boolean | - | false | Loading state |
| isSuccess | boolean | - | false | Success state |
| successComponent | ReactNode | - | - | Success state content |
| noDivider | boolean | - | false | Hide header divider |
| noHeading | boolean | - | false | Hide header |
| noCloseButton | boolean | - | false | Hide close button |
API Documentation
WPModal
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| heading | string | ✓ | - | Modal title |
| isOpen | boolean | ✓ | - | Whether modal is open |
| handleClose | function | ✓ | - | Close handler |
| children | ReactNode | ✓ | - | Modal content |
| isLoading | boolean | - | false | Loading state |
| customWidth | string | - | - | Custom modal width |
| isCentered | boolean | - | false | Center modal |
Usage Example
import WPModal from 'components/WPModal';
function MyModal() {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button onClick={onOpen}>Open Modal</Button>
<WPModal
heading='Edit Profile'
isOpen={isOpen}
handleClose={onClose}
customWidth='600px'
>
<Text>Modal content goes here</Text>
</WPModal>
</>
);
}
ActionModal
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| isOpen | boolean | ✓ | - | Whether modal is open |
| onClose | function | ✓ | - | Close handler |
| children | ReactNode | - | - | Modal content |
| heading | string/ReactNode | - | - | Modal title |
| footer | ReactNode | - | - | Custom footer |
| isSuccess | boolean | - | false | Success state |
| successComponent | ReactNode | - | - | Success content |
| noDivider | boolean | - | false | Hide divider |
| noHeading | boolean | - | false | Hide heading |
| noCloseButton | boolean | - | false | Hide close button |
| width | string | - | 'full' | Modal width |
| maxW | string | - | '630px' | Max width |
| minW | string | - | - | Min width |
| maxH | string | - | - | Max height |
| minH | string | - | - | Min height |
| borderRadius | string | - | 'md' | Border radius |
| description | string | - | - | Modal description |
| headingStyles | object | - | - | Heading styles |
| footerStyles | object | - | - | Footer styles |
Usage Example
import ActionModal from 'components/Forms/Modals/ActionModal';
function CreateUserModal() {
const { isOpen, onClose } = useDisclosure();
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const handleSubmit = async () => {
setIsLoading(true);
try {
await createUser();
setIsSuccess(true);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<ActionModal
isOpen={isOpen}
onClose={onClose}
heading='Create New User'
description='Fill in the details to create a new user'
isSuccess={isSuccess}
footer={
<HStack>
<Button variant='outline' onClick={onClose}>
Cancel
</Button>
<Button
onClick={handleSubmit}
isLoading={isLoading}
loadingText='Creating...'
>
Create User
</Button>
</HStack>
}
>
<VStack spacing={4}>
<FormControl>
<FormLabel>Name</FormLabel>
<Input placeholder='Enter name' />
</FormControl>
<FormControl>
<FormLabel>Email</FormLabel>
<Input placeholder='Enter email' />
</FormControl>
</VStack>
</ActionModal>
);
}
ActionConfirmationModal
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| isOpen | boolean | ✓ | - | Whether modal is open |
| onClose | function | ✓ | - | Close handler |
| children | ReactNode | - | - | Modal content |
| heading | string/ReactNode | - | - | Modal title |
| handleActionClick | function | - | - | Action handler |
| isLoading | boolean | - | false | Loading state |
| loadingText | string | - | 'Submitting...' | Loading text |
| rightButtonText | string | - | 'Submit' | Right button text |
| leftButtonText | string | - | 'Cancel' | Left button text |
| isSuccess | boolean | - | false | Success state |
| successMessage | string | - | - | Success message |
| disableCloseButton | boolean | - | false | Disable close button |
| noFooter | boolean | - | false | Hide footer |
| noDivider | boolean | - | false | Hide divider |
| isDisabled | boolean | - | false | Disable action button |
Usage Example
import ActionConfirmationModal from 'components/Forms/Modals/ActionConfirmationModal';
function DeleteUserModal() {
const { isOpen, onClose } = useDisclosure();
const [isLoading, setIsLoading] = useState(false);
const handleDelete = async () => {
setIsLoading(true);
try {
await deleteUser();
onClose();
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<ActionConfirmationModal
isOpen={isOpen}
onClose={onClose}
heading='Delete User'
handleActionClick={handleDelete}
isLoading={isLoading}
loadingText='Deleting...'
rightButtonText='Delete'
leftButtonText='Cancel'
>
<Text>
Are you sure you want to delete this user? This action cannot be undone.
</Text>
</ActionConfirmationModal>
);
}
DeleteConfirmationModal
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| isOpen | boolean | ✓ | - | Whether modal is open |
| onClose | function | ✓ | - | Close handler |
| children | ReactNode | ✓ | - | Modal content |
| handleActionClick | function | ✓ | - | Delete handler |
| isLoading | boolean | ✓ | - | Loading state |
| loadingText | string | - | 'Deleting...' | Loading text |
| heading | string | - | - | Modal title |
| rightButtonText | string | - | 'Delete' | Delete button text |
| leftButtonText | string | - | 'Cancel' | Cancel button text |
| isCentered | boolean | - | true | Center modal |
Usage Example
import DeleteConfirmationModal from 'components/Forms/Modals/DeleteModal';
function DeleteItemModal() {
const { isOpen, onClose } = useDisclosure();
const [isLoading, setIsLoading] = useState(false);
const handleDelete = async () => {
setIsLoading(true);
try {
await deleteItem();
onClose();
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<DeleteConfirmationModal
isOpen={isOpen}
onClose={onClose}
heading='Delete Item'
handleActionClick={handleDelete}
isLoading={isLoading}
>
<Text>
Are you sure you want to delete this item? This action cannot be undone.
</Text>
</DeleteConfirmationModal>
);
}
SuccessResponseModal
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| isOpen | boolean | ✓ | - | Whether modal is open |
| onClose | function | ✓ | - | Close handler |
| children | ReactNode | - | - | Modal content |
| heading | string | - | - | Modal title |
| disableCloseButton | boolean | - | false | Disable close button |
Usage Example
import SuccessResponseModal from 'components/Forms/Modals/SuccessResponseModal';
function SuccessModal() {
const { isOpen, onClose } = useDisclosure();
return (
<SuccessResponseModal isOpen={isOpen} onClose={onClose} heading='Success!'>
<Text>Your action was completed successfully.</Text>
</SuccessResponseModal>
);
}
ExportModal
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| title | string | - | - | Export title |
| isOpen | boolean | ✓ | - | Whether modal is open |
| onClose | function | ✓ | - | Close handler |
| columnsOptions | Array | ✓ | - | Available columns |
| onExport | function | ✓ | - | Export handler |
| isLoading | boolean | - | false | Loading state |
Usage Example
import ExportModal from 'components/Forms/Modals/ExportModal';
function DataExportModal() {
const { isOpen, onClose } = useDisclosure();
const [isLoading, setIsLoading] = useState(false);
const columnsOptions = [
{ value: 'name', label: 'Name' },
{ value: 'email', label: 'Email' },
{ value: 'role', label: 'Role' },
];
const handleExport = async exportData => {
setIsLoading(true);
try {
await exportData();
onClose();
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<ExportModal
title='Users'
isOpen={isOpen}
onClose={onClose}
columnsOptions={columnsOptions}
onExport={handleExport}
isLoading={isLoading}
/>
);
}
Modal Patterns
Basic Modal Usage
import { useDisclosure } from '@chakra-ui/react';
import WPModal from 'components/WPModal';
function BasicModalExample() {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button onClick={onOpen}>Open Modal</Button>
<WPModal
heading='Information'
isOpen={isOpen}
handleClose={onClose}
customWidth='500px'
>
<Text>This is a basic modal with some information.</Text>
</WPModal>
</>
);
}
Form Modal
import { useForm } from 'react-hook-form';
import ActionModal from 'components/Forms/Modals/ActionModal';
function FormModalExample() {
const { isOpen, onClose } = useDisclosure();
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const [isLoading, setIsLoading] = useState(false);
const onSubmit = async data => {
setIsLoading(true);
try {
await saveData(data);
onClose();
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<ActionModal
isOpen={isOpen}
onClose={onClose}
heading='Add New Item'
footer={
<HStack>
<Button variant='outline' onClick={onClose}>
Cancel
</Button>
<Button
type='submit'
form='item-form'
isLoading={isLoading}
loadingText='Saving...'
>
Save
</Button>
</HStack>
}
>
<form id='item-form' onSubmit={handleSubmit(onSubmit)}>
<VStack spacing={4}>
<FormControl isInvalid={errors.name}>
<FormLabel>Name</FormLabel>
<Input
{...register('name', { required: 'Name is required' })}
placeholder='Enter name'
/>
<FormErrorMessage>{errors.name?.message}</FormErrorMessage>
</FormControl>
<FormControl isInvalid={errors.description}>
<FormLabel>Description</FormLabel>
<Textarea
{...register('description')}
placeholder='Enter description'
/>
</FormControl>
</VStack>
</form>
</ActionModal>
);
}
Confirmation Modal
import ActionConfirmationModal from 'components/Forms/Modals/ActionConfirmationModal';
function ConfirmationModalExample() {
const { isOpen, onOpen, onClose } = useDisclosure();
const [isLoading, setIsLoading] = useState(false);
const handleConfirm = async () => {
setIsLoading(true);
try {
await performAction();
onClose();
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<>
<Button onClick={onOpen} colorScheme='red'>
Delete Item
</Button>
<ActionConfirmationModal
isOpen={isOpen}
onClose={onClose}
heading='Confirm Deletion'
handleActionClick={handleConfirm}
isLoading={isLoading}
loadingText='Deleting...'
rightButtonText='Delete'
leftButtonText='Cancel'
>
<Text>
Are you sure you want to delete this item? This action cannot be
undone.
</Text>
</ActionConfirmationModal>
</>
);
}
Success Modal with Auto-close
import SuccessResponseModal from 'components/Forms/Modals/SuccessResponseModal';
function AutoCloseSuccessModal() {
const { isOpen, onOpen, onClose } = useDisclosure();
// Auto-close after 3 seconds
useEffect(() => {
if (isOpen) {
const timer = setTimeout(() => {
onClose();
}, 3000);
return () => clearTimeout(timer);
}
}, [isOpen, onClose]);
return (
<>
<Button onClick={onOpen}>Show Success</Button>
<SuccessResponseModal
isOpen={isOpen}
onClose={onClose}
heading='Success!'
>
<VStack spacing={4}>
<Text>Your action was completed successfully.</Text>
<Text fontSize='sm' color='gray.500'>
This modal will close automatically in 3 seconds.
</Text>
</VStack>
</SuccessResponseModal>
</>
);
}
Multi-step Modal
import ActionModal from 'components/Forms/Modals/ActionModal';
function MultiStepModal() {
const { isOpen, onClose } = useDisclosure();
const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const nextStep = () => setCurrentStep(prev => prev + 1);
const prevStep = () => setCurrentStep(prev => prev - 1);
const handleFinish = async () => {
setIsLoading(true);
try {
await completeProcess();
onClose();
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
const getFooter = () => {
if (currentStep === 1) {
return (
<HStack>
<Button variant='outline' onClick={onClose}>
Cancel
</Button>
<Button onClick={nextStep}>Next</Button>
</HStack>
);
} else if (currentStep === 2) {
return (
<HStack>
<Button variant='outline' onClick={prevStep}>
Back
</Button>
<Button onClick={handleFinish} isLoading={isLoading}>
Finish
</Button>
</HStack>
);
}
};
return (
<ActionModal
isOpen={isOpen}
onClose={onClose}
heading={`Step ${currentStep} of 2`}
footer={getFooter()}
size='lg'
>
{currentStep === 1 && (
<VStack spacing={4}>
<Text>Step 1 content</Text>
<Input placeholder='Enter details' />
</VStack>
)}
{currentStep === 2 && (
<VStack spacing={4}>
<Text>Step 2 content</Text>
<Textarea placeholder='Additional information' />
</VStack>
)}
</ActionModal>
);
}
Styling & Theming
Modal Sizes
// Available sizes
<ActionModal size="xs" /> // Extra small
<ActionModal size="sm" /> // Small
<ActionModal size="md" /> // Medium (default)
<ActionModal size="lg" /> // Large
<ActionModal size="xl" /> // Extra large
<ActionModal size="2xl" /> // 2X large
<ActionModal size="3xl" /> // 3X large
<ActionModal size="4xl" /> // 4X large
<ActionModal size="5xl" /> // 5X large
<ActionModal size="6xl" /> // 6X large
<ActionModal size="full" /> // Full screen
Custom Styling
<ActionModal
isOpen={isOpen}
onClose={onClose}
heading='Custom Styled Modal'
headingStyles={{
fontSize: '24px',
color: 'blue.500',
textTransform: 'uppercase',
}}
borderRadius='xl'
maxW='800px'
footerStyles={{
backgroundColor: 'gray.50',
borderTop: '1px solid',
borderColor: 'gray.200',
}}
>
<Text>Custom styled modal content</Text>
</ActionModal>
Responsive Modals
<ActionModal
isOpen={isOpen}
onClose={onClose}
heading='Responsive Modal'
size={{ base: 'full', md: 'lg' }}
maxW={{ base: '100%', md: '600px' }}
>
<Text>This modal adapts to screen size</Text>
</ActionModal>
Modal Context & Management
Modal Provider Pattern
import { createContext, useContext, useState } from 'react';
const ModalContext = createContext();
export const ModalProvider = ({ children }) => {
const [modals, setModals] = useState({});
const openModal = id => {
setModals(prev => ({ ...prev, [id]: true }));
};
const closeModal = id => {
setModals(prev => ({ ...prev, [id]: false }));
};
return (
<ModalContext.Provider value={{ modals, openModal, closeModal }}>
{children}
</ModalContext.Provider>
);
};
export const useModal = id => {
const { modals, openModal, closeModal } = useContext(ModalContext);
return {
isOpen: modals[id] || false,
onOpen: () => openModal(id),
onClose: () => closeModal(id),
};
};
Global Modal Manager
import { useDisclosure } from '@chakra-ui/react';
const useGlobalModal = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [modalType, setModalType] = useState(null);
const [modalProps, setModalProps] = useState({});
const openModal = (type, props = {}) => {
setModalType(type);
setModalProps(props);
onOpen();
};
const closeModal = () => {
setModalType(null);
setModalProps({});
onClose();
};
return {
isOpen,
modalType,
modalProps,
openModal,
closeModal,
};
};
// Usage
function App() {
const { isOpen, modalType, modalProps, openModal, closeModal } =
useGlobalModal();
const renderModal = () => {
switch (modalType) {
case 'delete':
return <DeleteConfirmationModal {...modalProps} />;
case 'success':
return <SuccessResponseModal {...modalProps} />;
default:
return null;
}
};
return (
<Box>
<Button onClick={() => openModal('delete', { itemId: 1 })}>
Delete Item
</Button>
{renderModal()}
</Box>
);
}
Accessibility
ARIA Support
- All modals include proper ARIA labels and roles
- Modal content is properly announced to screen readers
- Focus management is handled automatically
- Modal overlay prevents interaction with background content
Keyboard Navigation
- Escape key closes modals
- Tab navigation is trapped within modal
- Focus returns to trigger element on close
- Enter/Space activates buttons
Screen Reader Support
- Modal titles are announced
- Loading states are communicated
- Error messages are accessible
- Success states are announced
Testing
Unit Testing
import { render, screen, fireEvent } from '@testing-library/react';
import ActionModal from 'components/Forms/Modals/ActionModal';
test('renders modal when open', () => {
const onClose = jest.fn();
render(
<ActionModal isOpen={true} onClose={onClose} heading='Test Modal'>
<div>Modal content</div>
</ActionModal>,
);
expect(screen.getByText('Test Modal')).toBeInTheDocument();
expect(screen.getByText('Modal content')).toBeInTheDocument();
});
test('calls onClose when close button clicked', () => {
const onClose = jest.fn();
render(
<ActionModal isOpen={true} onClose={onClose} heading='Test Modal'>
<div>Content</div>
</ActionModal>,
);
fireEvent.click(screen.getByRole('button', { name: /close/i }));
expect(onClose).toHaveBeenCalled();
});
Integration Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ActionConfirmationModal from 'components/Forms/Modals/ActionConfirmationModal';
test('confirmation modal workflow', async () => {
const onClose = jest.fn();
const handleAction = jest.fn();
render(
<ActionConfirmationModal
isOpen={true}
onClose={onClose}
heading='Delete Item'
handleActionClick={handleAction}
rightButtonText='Delete'
leftButtonText='Cancel'
>
<div>Are you sure?</div>
</ActionConfirmationModal>,
);
// Test cancel
fireEvent.click(screen.getByText('Cancel'));
expect(onClose).toHaveBeenCalled();
// Test confirm
fireEvent.click(screen.getByText('Delete'));
expect(handleAction).toHaveBeenCalled();
});
Performance Optimization
Lazy Loading
const ActionModal = lazy(() => import('components/Forms/Modals/ActionModal'));
function LazyModal() {
const { isOpen, onClose } = useDisclosure();
return (
<Suspense fallback={<div>Loading...</div>}>
{isOpen && (
<ActionModal isOpen={isOpen} onClose={onClose} heading='Lazy Modal'>
<Text>This modal is loaded lazily</Text>
</ActionModal>
)}
</Suspense>
);
}
Conditional Rendering
function ConditionalModal() {
const { isOpen, onClose } = useDisclosure();
return (
<>
<Button onClick={onOpen}>Open Modal</Button>
{isOpen && (
<ActionModal
isOpen={isOpen}
onClose={onClose}
heading='Conditional Modal'
>
<Text>Only rendered when needed</Text>
</ActionModal>
)}
</>
);
}
Best Practices
- State Management: Use
useDisclosurefor modal state - Focus Management: Let Chakra UI handle focus automatically
- Accessibility: Always provide meaningful headings and descriptions
- Performance: Use conditional rendering and lazy loading
- User Experience: Provide clear actions and feedback
- Error Handling: Include proper error states and messages
- Mobile Responsiveness: Test on different screen sizes
- Escape Routes: Always provide ways to cancel or close
Common Issues
Modal Not Opening
- Check if
isOpenprop is correctly set - Verify modal is not conditionally rendered when closed
- Ensure parent component state is updating
Focus Issues
- Let Chakra UI handle focus management
- Use
initialFocusRefandfinalFocusRefif needed - Avoid manual focus manipulation
Z-index Problems
- Use Chakra UI's theme z-index values
- Avoid conflicting z-index values
- Test with other overlays (dropdowns, tooltips)
Mobile Issues
- Test on different screen sizes
- Use responsive modal sizes
- Ensure touch targets are adequate
Migration Guide
From Basic Modal to ActionModal
// Before
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Title</ModalHeader>
<ModalCloseButton />
<ModalBody>Content</ModalBody>
<ModalFooter>
<Button onClick={onClose}>Close</Button>
</ModalFooter>
</ModalContent>
</Modal>
// After
<ActionModal
isOpen={isOpen}
onClose={onClose}
heading="Title"
footer={<Button onClick={onClose}>Close</Button>}
>
Content
</ActionModal>
Updating Props
- Replace
onClosewithhandleClosefor WPModal - Use
headinginstead of separate header components - Consolidate footer elements into
footerprop - Use boolean flags for common variations (
noDivider,noCloseButton)
This completes the comprehensive documentation for the Modal components in the WorkPayCore frontend application.