logo
eng-flag

Building a Modal Component from Scratch in React

Introduction

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.

Table of Contents

  1. Basic Modal Structure
  2. Styling the Modal
  3. Adding Animations
  4. Implementing Accessibility
  5. Advanced Features
  6. Best Practices and Optimization
  7. Conclusion

Basic Modal Structure

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.

Styling the Modal

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}>
          &times;
        </button>
        {children}
      </div>
    </div>
  );
};

export default Modal;

Adding Animations

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}>
          &times;
        </button>
        {children}
      </div>
    </div>
  );
};

export default Modal;

Implementing Accessibility

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"
        >
          &times;
        </button>
        {children}
      </div>
    </div>
  );
};

export default Modal;

These changes improve accessibility by:

  1. Adding appropriate ARIA attributes
  2. Implementing keyboard navigation (Escape key to close)
  3. Managing focus when the modal opens
  4. Preventing scroll on the body when the modal is open

Advanced Features

Now that we have a solid foundation, let's add some advanced features to our modal component.

1. Customizable Transitions

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;

2. Portal for Rendering

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;

3. Custom Close Trigger

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"
          >
            &times;
          </button>
        )}
        {children}
      </div>
    </div>,
    document.body
  );
};

Best Practices and Optimization

  1. Memoization: Use React.memo to prevent unnecessary re-renders of the Modal component.
export default React.memo(Modal);
  1. Custom Hook: Create a custom hook for managing modal state:
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 };
};
  1. Prop Types: Use PropTypes for type checking:
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,
};
  1. Testing: Write unit tests for your modal component using tools like Jest and React Testing Library.

2024 © All rights reserved - buraxta.com