Accessible modals in Gatsby using portals

Accessible modals in Gatsby using portals

and additional react hooks

Adding a modal (dialog) to your website or app is a common scenario. How can we do it in React? Specifically in Gatsby? Before React version 16, developers had to hack their way a bit to create this simple element. It’s because there wasn’t a straightforward way to create a DOM element next to root/parent element, which could cause issues with positioning (z-index, overflow: hidden). React v16 introduced a new feature called Portals that enables just that.

Photo by [Zoltan Tasi](https://cdn.hashnode.com/res/hashnode/image/upload/v1621430439350/SvcPTwZrU.html) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)Photo by Zoltan Tasi on Unsplash

React portals

In the case of Gatsby, the DOM would look like this after implementing React portals.

<html>
  <body>
    <div id="___gatsby"></div>
    <div id="portal"></div>
  </body>
</html>

#___gatsby is the parent element and #portal is a new sibling node, in which we can inject our modals for example.

This is how we can create a portal:

ReactDOM.createPortal(child, container)

The first argument (child) is any renderable React child, such as an element, string, or fragment. The second argument (container) is a DOM element.

So, in HTML we could add a new element #portal like above, access it

var container = document.getElementById('portal')

and pass it to createPortal() as a 2nd argument.

Important note:

Even though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way. Features like context work exactly the same regardless of whether the child is a portal, as the portal still exists in the React tree regardless of the position in the DOM tree.

How can we do it in Gatsby?

gatsby-plugin-portal

If we want to use portals in Gatsby, there’s a plugin we can use that handles some disconcerting challenges for us. Two of them are:

  • index.html doesn’t exist in Gatsby (only after build)

  • The document object is not defined during build.

We can install the plugin by running the following command:

npm install --save gatsby-plugin-portal

And then in gatsby-config.js we add:

module.exports = {
  plugins: [`gatsby-plugin-portal`]
}

There are 2 options you could specify, the key and id of the created element, which are set to ‘portal’ by default.

The plugin adds the div component using *onRenderBody*API for us but we still need to cover the 2nd point. It’s time to create portal.js file, which takes care of that.

portal.js

// Example from gatsby-plugin-portal
// https://www.gatsbyjs.com/plugins/gatsby-plugin-portal/?=gatsby-plugin-portal#gatsby-gotcha---document-is-undefined

import { Component } from 'react'
import ReactDOM from 'react-dom'

// Use a ternary operator to make sure that the document object is defined
const portalRoot = typeof document !== `undefined` ? document.getElementById('portal') : null

export default class Portal extends Component {

  constructor() {
    super()
    // Use a ternary operator to make sure that the document object is defined
    this.el = typeof document !== `undefined` ? document.createElement('div') : null
  }

  componentDidMount = () => {
    portalRoot.appendChild(this.el)
  }

  componentWillUnmount = () => {
    portalRoot.removeChild(this.el)
  }

  render() {
    const { children } = this.props

    // Check that this.el is not null before using ReactDOM.createPortal
    if (this.el) {
      return ReactDOM.createPortal(children, this.el)
    } else {
      return null
    }

  }
}

There are a few checks for the document model that it’s created.

Let’s create a modal component now.

Modals and additional hooks

How you create your modals depends on the project and your needs, but I’d like to build with you an accessible modal component using some React hooks and interesting techniques like the following:

Ugh! Do we need all this to create a simple modal window?

I hear you, but don’t worry it’s going to be just a few lines of code. Let me show you the complete code first.

modal.js

import React, { useState, forwardRef, useImperativeHandle } from "react"
import Portal from "./Portal"

const Modal = forwardRef((props, ref) => {

  const [display, setDisplay] = useState(false)

  useImperativeHandle(
    ref,
    () => {
      return {
        openModal: () => handleOpen(),
        closeModal: () => handleClose(),
      }
    }
  )

  const handleOpen = () => {
    setDisplay(true);
  }

  const handleClose = () => {
    setDisplay(false);
  }

  if (display) {
    return (
      <Portal>
          <div className="modal-backdrop"></div>
          <div className="modal-container">
            {props.children}
            <button onClick={handleClose}>close</button>
          </div>
      </Portal>
    )
  }

  return null
})

export default Modal

Modal component is ready, so we can import it in other components (for example about.js) and use it like this:

about.js

import React, {useRef} from 'react';
**import Modal from './modal';**

export default function About() {
    const modalRef1 = useRef();

    return (
        <section>
            <button className="btn" onClick={() => modalRef1.current.openModal()}>Open modal</button>

            <Modal ref={modalRef1}>
                <h3>Modal title 1</h3>
            </Modal>
        </section>
    )
}

Let’s now walk through the code above.

useState() hook

*const* [display, setDisplay] = useState(false)

Default state is false, which means the modal is closed. We have 2 functions for displaying/hiding modal where we call setDisplay() accordingly. True or false value is stored in display variable, that we check before rendering our Modal (Portal).

useRef and forwardRef

*Ref forwarding is a technique for automatically passing a ref through a component to one of its children. useRef() returns a mutable ref object whose .current property is initialized.*

  1. We create a variable modalRef1 in the About component, where we want to display the modal and we assign useRef() to the variable. *const **modalRef1 = useRef()**;*

  2. We pass our ref down to modal by specifying it as a JSX attribute. <Modal **ref={modalRef1}**>so it means the .current property (from useRef) will point to this element.

  3. React passes the ref to the (props, ref) => ... function inside forwardRef as a second argument. (in modal.js) const Modal = **forwardRef((props, ref)** => { // modal.js

useImperativeHandle() hook

*useImperativeHandle customizes the instance value that is exposed to parent components when using ref.*

In the modal.js we can use the ref we are passing in forwardRef((props, ref) in the useImperativeHandle() hook. The syntax looks like this:

useImperativeHandle(ref, createHandle, [deps])

In the place of createHandle we return an object with 2 properties: openModal and closeModal, which are both functions.

useImperativeHandle(
    ref,
    () => {
      return {
        openModal: () => handleOpen(),
        closeModal: () => handleClose(),
      }
    }
  )

This means we are customizing the current property of ref. In about.js we use our <Modal> component and the ref is pointing to it. Ref is forwarded, modal.js receives it and it’s used in the useImperativeHandle() hook that returns openModal() and closeModal() functions. Therefore we can then call these functions for example when we click a button:

<button onClick={() => **modalRef1.current.openModal()**}>Open modal</button>

Dylan Kerler in his article put it this way:

*useImperativeHandle provides a middle ground between redux and props for bidirectional data and logic flow.*

I think that’s quite useful in certain scenarios. And remember it should be used with [forwardRef](https://reactjs.org/docs/react-api.html#reactforwardref).

Check out **this Sandbox *made by user daryanka to see useImperativeHandle()* in action.

Accesibility

One thing we should have in mind as web developers is accessibility. This topic would deserve an article on its own, so I am just going to quickly list a few important things to consider when it comes to modals.

Roles and attributes

The modals should contain the following attributes.

<div role="dialog"
     aria-modal="true"
     aria-labelledby="dialog1_label">

In Gatsby, if we want to use portals just for modals for example, we could update portal.js and add these lines to constructor, where we create the div.

this.el.setAttribute("role", "dialog");
this.el.setAttribute("aria-modal", "true");

Close modal on ‘Escape’

One of the ways is to use useEffect() hook like this.

useEffect(() => {
    const close = e => {
        if (e.keyCode === 27) {
            handleClose();
        }
    }
    window.addEventListener('keydown', close)
    return () => window.removeEventListener('keydown', close)
  }, []);

Apart from Escape button we should consider using Tab and Shift + Tab to move around the screen. Where we set the focus is important but that also depends on how you structure your content. That being said, I will leave here a few links related to accessibility:

Other references:

Thank you!