Skip to main content

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.

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
  • 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:

PropTypeRequiredDefaultDescription
isOpenboolean-Whether modal is open
onClosefunction-Close handler function
childrenReactNode--Modal content
headingstring/ReactNode--Modal title
sizestring-'md'Modal size
isCenteredboolean-falseCenter modal vertically

Action Modal Props

PropTypeRequiredDefaultDescription
footerReactNode--Custom footer content
isLoadingboolean-falseLoading state
isSuccessboolean-falseSuccess state
successComponentReactNode--Success state content
noDividerboolean-falseHide header divider
noHeadingboolean-falseHide header
noCloseButtonboolean-falseHide close button

API Documentation

WPModal

Props

PropTypeRequiredDefaultDescription
headingstring-Modal title
isOpenboolean-Whether modal is open
handleClosefunction-Close handler
childrenReactNode-Modal content
isLoadingboolean-falseLoading state
customWidthstring--Custom modal width
isCenteredboolean-falseCenter 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'
>
&lt;Text&gt;Modal content goes here</Text>
</WPModal>
</>
);
}

ActionModal

Props

PropTypeRequiredDefaultDescription
isOpenboolean-Whether modal is open
onClosefunction-Close handler
childrenReactNode--Modal content
headingstring/ReactNode--Modal title
footerReactNode--Custom footer
isSuccessboolean-falseSuccess state
successComponentReactNode--Success content
noDividerboolean-falseHide divider
noHeadingboolean-falseHide heading
noCloseButtonboolean-falseHide close button
widthstring-'full'Modal width
maxWstring-'630px'Max width
minWstring--Min width
maxHstring--Max height
minHstring--Min height
borderRadiusstring-'md'Border radius
descriptionstring--Modal description
headingStylesobject--Heading styles
footerStylesobject--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={
&lt;HStack&gt;
<Button variant='outline' onClick={onClose}>
Cancel
</Button>
<Button
onClick={handleSubmit}
isLoading={isLoading}
loadingText='Creating...'
>
Create User
</Button>
</HStack>
}
>
<VStack spacing={4}>
&lt;FormControl&gt;
&lt;FormLabel&gt;Name</FormLabel>
<Input placeholder='Enter name' />
</FormControl>
&lt;FormControl&gt;
&lt;FormLabel&gt;Email</FormLabel>
<Input placeholder='Enter email' />
</FormControl>
</VStack>
</ActionModal>
);
}

ActionConfirmationModal

Props

PropTypeRequiredDefaultDescription
isOpenboolean-Whether modal is open
onClosefunction-Close handler
childrenReactNode--Modal content
headingstring/ReactNode--Modal title
handleActionClickfunction--Action handler
isLoadingboolean-falseLoading state
loadingTextstring-'Submitting...'Loading text
rightButtonTextstring-'Submit'Right button text
leftButtonTextstring-'Cancel'Left button text
isSuccessboolean-falseSuccess state
successMessagestring--Success message
disableCloseButtonboolean-falseDisable close button
noFooterboolean-falseHide footer
noDividerboolean-falseHide divider
isDisabledboolean-falseDisable 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'
>
&lt;Text&gt;
Are you sure you want to delete this user? This action cannot be undone.
</Text>
</ActionConfirmationModal>
);
}

DeleteConfirmationModal

Props

PropTypeRequiredDefaultDescription
isOpenboolean-Whether modal is open
onClosefunction-Close handler
childrenReactNode-Modal content
handleActionClickfunction-Delete handler
isLoadingboolean-Loading state
loadingTextstring-'Deleting...'Loading text
headingstring--Modal title
rightButtonTextstring-'Delete'Delete button text
leftButtonTextstring-'Cancel'Cancel button text
isCenteredboolean-trueCenter 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}
>
&lt;Text&gt;
Are you sure you want to delete this item? This action cannot be undone.
</Text>
</DeleteConfirmationModal>
);
}

SuccessResponseModal

Props

PropTypeRequiredDefaultDescription
isOpenboolean-Whether modal is open
onClosefunction-Close handler
childrenReactNode--Modal content
headingstring--Modal title
disableCloseButtonboolean-falseDisable 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!'>
&lt;Text&gt;Your action was completed successfully.</Text>
</SuccessResponseModal>
);
}

ExportModal

Props

PropTypeRequiredDefaultDescription
titlestring--Export title
isOpenboolean-Whether modal is open
onClosefunction-Close handler
columnsOptionsArray-Available columns
onExportfunction-Export handler
isLoadingboolean-falseLoading 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}
/>
);
}

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'
>
&lt;Text&gt;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={
&lt;HStack&gt;
<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}>
&lt;FormLabel&gt;Name</FormLabel>
<Input
{...register('name', { required: 'Name is required' })}
placeholder='Enter name'
/>
&lt;FormErrorMessage&gt;{errors.name?.message}</FormErrorMessage>
</FormControl>

<FormControl isInvalid={errors.description}>
&lt;FormLabel&gt;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'
>
&lt;Text&gt;
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}>
&lt;Text&gt;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 (
&lt;HStack&gt;
<Button variant='outline' onClick={onClose}>
Cancel
</Button>
<Button onClick={nextStep}>Next</Button>
</HStack>
);
} else if (currentStep === 2) {
return (
&lt;HStack&gt;
<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}>
&lt;Text&gt;Step 1 content</Text>
<Input placeholder='Enter details' />
</VStack>
)}
{currentStep === 2 && (
<VStack spacing={4}>
&lt;Text&gt;Step 2 content</Text>
<Textarea placeholder='Additional information' />
</VStack>
)}
</ActionModal>
);
}

Styling & Theming

// 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',
}}
>
&lt;Text&gt;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' }}
>
&lt;Text&gt;This modal adapts to screen size</Text>
</ActionModal>
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 (
&lt;Box&gt;
<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'>
&lt;Text&gt;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'
>
&lt;Text&gt;Only rendered when needed</Text>
</ActionModal>
)}
</>
);
}

Best Practices

  1. State Management: Use useDisclosure for modal state
  2. Focus Management: Let Chakra UI handle focus automatically
  3. Accessibility: Always provide meaningful headings and descriptions
  4. Performance: Use conditional rendering and lazy loading
  5. User Experience: Provide clear actions and feedback
  6. Error Handling: Include proper error states and messages
  7. Mobile Responsiveness: Test on different screen sizes
  8. Escape Routes: Always provide ways to cancel or close

Common Issues

  • Check if isOpen prop 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 initialFocusRef and finalFocusRef if 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 />
&lt;ModalContent&gt;
&lt;ModalHeader&gt;Title</ModalHeader>
<ModalCloseButton />
&lt;ModalBody&gt;Content</ModalBody>
&lt;ModalFooter&gt;
<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 onClose with handleClose for WPModal
  • Use heading instead of separate header components
  • Consolidate footer elements into footer prop
  • Use boolean flags for common variations (noDivider, noCloseButton)

This completes the comprehensive documentation for the Modal components in the WorkPayCore frontend application.