Modals are essential UI elements in modern web applications, providing a way to display content that temporarily blocks interactions with the main view. In this comprehensive guide, we'll walk through the process of building a reusable modal component from scratch using React. We'll cover everything from basic structure to advanced features and accessibility considerations.
Let's start by creating the basic structure of our modal component. We'll use React's built-in useState
hook to manage the modal's open/close state.
import React, { useState } from 'react';
const Modal = ({ isOpen, onClose, children }) => {
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content">
<button className="modal-close" onClick={onClose}>
Close
</button>
{children}
</div>
</div>
);
};
const App = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
return (
<div>
<button onClick={openModal}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={closeModal}>
<h2>Modal Content</h2>
<p>This is the content of the modal.</p>
</Modal>
</div>
);
};
export default App;
This basic structure provides a functional modal that can be opened and closed. The Modal
component receives isOpen
, onClose
, and children
props, allowing for flexible usage.
Now, let's add some CSS to make our modal look better. We'll use CSS modules to keep our styles scoped to the component.
Create a new file called Modal.module.css
:
.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modalContent {
background-color: white;
padding: 20px;
border-radius: 4px;
max-width: 500px;
width: 100%;
position: relative;
}
.modalClose {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
Now, update the Modal
component to use these styles:
import React from 'react';
import styles from './Modal.module.css';
const Modal = ({ isOpen, onClose, children }) => {
if (!isOpen) return null;
return (
<div className={styles.modalOverlay}>
<div className={styles.modalContent}>
<button className={styles.modalClose} onClick={onClose}>
×
</button>
{children}
</div>
</div>
);
};
export default Modal;
To make our modal more visually appealing, let's add some animations using CSS transitions. Update the Modal.module.css
file:
.modalOverlay {
/* ... existing styles ... */
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.modalOverlayActive {
opacity: 1;
}
.modalContent {
/* ... existing styles ... */
transform: scale(0.7);
transition: transform 0.3s ease-in-out;
}
.modalContentActive {
transform: scale(1);
}
Now, update the Modal
component to use these new classes:
import React, { useState, useEffect } from 'react';
import styles from './Modal.module.css';
const Modal = ({ isOpen, onClose, children }) => {
const [isActive, setIsActive] = useState(false);
useEffect(() => {
if (isOpen) {
setIsActive(true);
} else {
setIsActive(false);
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div className={`${styles.modalOverlay} ${isActive ? styles.modalOverlayActive : ''}`}>
<div className={`${styles.modalContent} ${isActive ? styles.modalContentActive : ''}`}>
<button className={styles.modalClose} onClick={onClose}>
×
</button>
{children}
</div>
</div>
);
};
export default Modal;
Accessibility is crucial for creating inclusive web applications. Let's enhance our modal to be more accessible:
import React, { useState, useEffect, useRef } from 'react';
import styles from './Modal.module.css';
const Modal = ({ isOpen, onClose, children, title }) => {
const [isActive, setIsActive] = useState(false);
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
setIsActive(true);
document.body.style.overflow = 'hidden';
modalRef.current?.focus();
} else {
setIsActive(false);
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
if (!isOpen) return null;
return (
<div
className={`${styles.modalOverlay} ${isActive ? styles.modalOverlayActive : ''}`}
onClick={onClose}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div
className={`${styles.modalContent} ${isActive ? styles.modalContentActive : ''}`}
onClick={(e) => e.stopPropagation()}
ref={modalRef}
tabIndex={-1}
onKeyDown={handleKeyDown}
>
<h2 id="modal-title" className={styles.modalTitle}>
{title}
</h2>
<button
className={styles.modalClose}
onClick={onClose}
aria-label="Close modal"
>
×
</button>
{children}
</div>
</div>
);
};
export default Modal;
These changes improve accessibility by:
Now that we have a solid foundation, let's add some advanced features to our modal component.
Let's make our transitions customizable by accepting props for duration and easing:
import React, { useState, useEffect, useRef } from 'react';
import styles from './Modal.module.css';
const Modal = ({
isOpen,
onClose,
children,
title,
transitionDuration = 300,
transitionTimingFunction = 'ease-in-out',
}) => {
// ... existing code ...
const overlayStyle = {
transition: `opacity ${transitionDuration}ms ${transitionTimingFunction}`,
};
const contentStyle = {
transition: `transform ${transitionDuration}ms ${transitionTimingFunction}`,
};
return (
<div
className={`${styles.modalOverlay} ${isActive ? styles.modalOverlayActive : ''}`}
style={overlayStyle}
// ... other attributes ...
>
<div
className={`${styles.modalContent} ${isActive ? styles.modalContentActive : ''}`}
style={contentStyle}
// ... other attributes ...
>
{/* ... existing content ... */}
</div>
</div>
);
};
export default Modal;
To ensure our modal is always rendered at the root level of the DOM, let's use React's createPortal
:
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import styles from './Modal.module.css';
const Modal = ({ isOpen, onClose, children, title, ...props }) => {
// ... existing code ...
return ReactDOM.createPortal(
<div
className={`${styles.modalOverlay} ${isActive ? styles.modalOverlayActive : ''}`}
// ... other attributes ...
>
{/* ... modal content ... */}
</div>,
document.body
);
};
export default Modal;
Let's add a prop that allows users to pass a custom close trigger:
const Modal = ({
isOpen,
onClose,
children,
title,
closeTrigger,
...props
}) => {
// ... existing code ...
return ReactDOM.createPortal(
<div
className={`${styles.modalOverlay} ${isActive ? styles.modalOverlayActive : ''}`}
// ... other attributes ...
>
<div
className={`${styles.modalContent} ${isActive ? styles.modalContentActive : ''}`}
// ... other attributes ...
>
<h2 id="modal-title" className={styles.modalTitle}>
{title}
</h2>
{closeTrigger ? (
React.cloneElement(closeTrigger, { onClick: onClose })
) : (
<button
className={styles.modalClose}
onClick={onClose}
aria-label="Close modal"
>
×
</button>
)}
{children}
</div>
</div>,
document.body
);
};
export default React.memo(Modal);
import { useState, useCallback } from 'react';
export const useModal = (initialState = false) => {
const [isOpen, setIsOpen] = useState(initialState);
const openModal = useCallback(() => setIsOpen(true), []);
const closeModal = useCallback(() => setIsOpen(false), []);
return { isOpen, openModal, closeModal };
};
import PropTypes from 'prop-types';
Modal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
title: PropTypes.string.isRequired,
transitionDuration: PropTypes.number,
transitionTimingFunction: PropTypes.string,
closeTrigger: PropTypes.element,
};
2024 © All rights reserved - buraxta.com