DEV Community

Cover image for Building Components in React: Modals
faisal khan
faisal khan

Posted on • Edited on

Building Components in React: Modals

A modal is a small UI element that will appear in the foreground of a website, usually triggered as a prompt for the user to do something

Let's build an ideal modal component in react from basics to advance


Enough talk


Table Of Contents

1. Creating a basic modal

A basic modal involves creating an overlay and inside the overlay, we render the modal component which will include the children passed by the consumer.

const Modal = ({
  position, // set the position of modal on viewport
  isFullScreen,
  modalStyle,
  containerStyle,
  height,
  children,
}) => {
  return (
    <ModalOverlay style={containerStyle}>
      <ModalComponent
        position={position}
        isFullScreen={isFullScreen}
        customHeight={height}
        style={modalStyle}
      >
        {children}
      </ModalComponent>
    </ModalOverlay>
  );
};

Modal.defaultProps = {
  position: "center",
  isFullScreen: false,
  height: "auto",
  modalStyle: {},
  containerStyle: {},
};

Modal.propTypes = {
  position: PropTypes.oneOf(["center", "top", "bottom"]),
  isFullScreen: PropTypes.bool,
  height: PropTypes.string,
  modalStyle: PropTypes.shape({}),
  containerStyle: PropTypes.shape({}),
  children: PropTypes.node.isRequired,
};

Enter fullscreen mode Exit fullscreen mode

2. Styling modal

For styling I have used styled-component

Since we have props such as position, height, isFullScreen we need to have conditional styling.

const ModalOverlay = styled.div`
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
  opacity: 1;
  position: fixed;
  overflow-x: hidden;
  overflow-y: auto;
  background-color: rgba(34, 34, 34, 0.8);
`;

const ModalComponent = styled.div`
  position: ${(props) =>
    props.position !== "center" ? "absolute" : "relative"};
  top: ${(props) => (props.position === "top" ? "0" : "auto")};
  bottom: ${(props) => (props.position === "bottom" ? "0" : "auto")};
  height: ${(props) => (props.isFullScreen ? "100%" : props.customHeight)};
  max-height: ${(props) => (props.isFullScreen ? "100%" : props.customHeight)};
  width: 100%;
`;
Enter fullscreen mode Exit fullscreen mode

3. Closing the modal

There are three ways to close a modal

  • Pressing ESC key
  • Clicking outside of the modal body
  • Clicking on close icon or button on the modal body which closes the modal
const Modal = ({ close, children }) => {
  const modalRef = useRef();
  const modalOverlayRef = useRef();

const handleClose = () => {
    close();
  };

const handleClick = (event) => {
    if (modalRef.current && !modalRef.current.contains(event.target)) {
      handleClose();
    }
  };

const handleKeyDown = (event) => {
    if (event.keyCode === 13) {
      return handleClose();
    }
  };

useEffect(() => {
    const modalOverlayRefCurrent = modalOverlayRef.current;
    modalOverlayRefCurrent.addEventListener("click", handleClick);
    document.addEventListener("keydown", handleKeyDown);
    return () => {
      document.removeEventListener("keydown", handleKeyDown);
      modalOverlayRefCurrent.removeEventListener("click", handleClick);
    };
  }, []);

return (
    <ModalOverlay ref={modalOverlayRef}>
      <ModalComponent ref={modalRef}>{children}</ModalComponent>
    </ModalOverlay>
  );
};

Enter fullscreen mode Exit fullscreen mode

4. Hardware backbutton to close the modal

One of the most searched questions on modals is how to close the modal by clicking the hardware back button on a mobile device.

One solution which I found to be working well is to leverage react-router in order to achieve it

We usually use state variable to show/hide the modal something like this

const [isModalVisible, setIsModalVisible] = useState(false)

const handleShowModal = () => {
  setIsModalVisible(true)
}

return isModalVisible ? <Modal/> : null
Enter fullscreen mode Exit fullscreen mode

We need to change the way we show/hide the modal component, instead of changing the state variable we will push a new route with a state variable like this

import { useHistory } from 'react-router-dom'

const historyHook = useHistory()

const handleShowModal = () => {
  history.push(window.location.pathname, { isModalVisible: true })
}

return historyHook.location.state.isModalVisible ? <Modal /> : null
Enter fullscreen mode Exit fullscreen mode

Now when the user clicks on to show the modal a new route is pushed with the same pathname but with a state variable named isModalVisible

Then, when a user clicks on the back button it will remove the route from the history stack thus closing the modal or we can simply call the below function

window.history.back() // closes the modal
Enter fullscreen mode Exit fullscreen mode

5. Making Modals More Usable And Accessible

Basic accessibility is a prerequisite for usability.

An accessible modal dialog is one where keyboard focus is managed properly, and the correct information is exposed to screen readers.

HTML and WAI-ARIA((Web Accessibility Initiative - Accessible Rich Internet Applications)) can be used to provide the necessary semantic information, CSS the appearance, and Javascript the behavior.

Three basics point to achieve accessibility in modal are:

-> Basic semantics has to be followed

The modal itself must be constructed from a combination of HTML and WAI-ARIA attributes, as in this example:

<div id="dialog" role="dialog" aria-labelledby="title" aria-describedby="description">
  <h1 id="title">Title</h1>
  <p id="description">Information</p>
  <button id="close" aria-label="close">×</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Note the dialog role, which tells assistive technologies that the element is a dialog.

The aria-labelledby and aria-describedby attributes are relationship attributes that connect the dialog to its title and description explicitly.

So when the focus is moved to the dialog or inside it, the text within those two elements will be read in succession.

-> Saving last active element

When a modal window loads, the element that the user last interacted with should be saved.

That way, when the modal window closes and the user returns to where they were, the focus on that element will have been maintained.

let lastFocus;

function handleShowModal () {
  lastFocus = document.activeElement; // save activeElement
}

function handleCloseModal () {
  lastFocus.focus(); // place focus on the saved element
}
Enter fullscreen mode Exit fullscreen mode

-> Shifting focus from main content to modal

When the modal loads, the focus should shift from the last active element either to the modal window itself or to the first interactive element in the modal, such as an input element.

const modal = document.getElementById('modal-id');

function modalShow () {
   modal.setAttribute('tabindex', '0');
   modal.focus();
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Component creation often involves multiple points to be kept in mind, right from creating a basic structure to solving common and complex problems such as accessibility and usability.

The article covers most parts of a modal and its uses and can easily be integrated into a live project.

Top comments (2)

Collapse
 
larsejaas profile image
Lars Ejaas • Edited

Nice article! You should probably also trap focus inside the modal. Otherwise users will be able to tab outside the modal using the keyboard. This is usually not desirable.

You can do this by mapping over all focusable elements in the modal, and once the user tab from the last element, the first element should gain focus. Furthermore shift-tab from first element should give focus to last element.

I just finished of a project written in React with a lot of different modals. Check it out at bruce-willis.rocks/en/ - there is even a url to the sourceCode at github 😊

Collapse
 
faisalpathan profile image
faisal khan

@larsejaas , completely agree trapping focus inside the modal is another important point to improve accessibility. Thanks for the solution as well. :)