peteris.rocks

Modal window in React from scratch

Creating a modal window component in React from scratch

Last updated on

I'd like to show how quick and easy it is to create a modal window component with ReactJS.

I needed a component for modal windows for a project and before reaching for Google I thought I'd try and see how hard it would be to create one from scratch.

Let's call our modal window component Modal and here is how we want to use it in our application.

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = { isModalOpen: false }
  }

  render() {
    return (
      <div>
        <button onClick={() => this.openModal()}>Open modal</button>
        <Modal isOpen={this.state.isModalOpen} onClose={() => this.closeModal()}>
          <h1>Modal title</h1>
          <p>hello</p>
          <p><button onClick={() => this.closeModal()}>Close</button></p>
        </Modal>
      </div>
    )
  }

  openModal() {
    this.setState({ isModalOpen: true })
  }

  closeModal() {
    this.setState({ isModalOpen: false })
  }
}

So when isModalOpen in our application component state is set to true, the modal will be displayed, otherwise it will be hidden. The way to open or close modal then is to simply set the value of isModalOpen.

Our modal component will be very simple. It will consist of content that will be absolutely positioned in the middle of the screen on top of all other elements on the page. And we will also have a grey background overlaying all other elements on the page.

Here is how to create the grey background overlay also known as backdrop.

<style>
  .backdrop {
    position: absolute;
    top: 0px;
    left: 0px;
    width: 100%;
    height: 100%;
    z-index: 9998;
    background: rgba(0, 0, 0, 0.3);
  }
</style>

<div class="backdrop"></div>

We make this div span the whole screen (width: 100% and height: 100%) and we make its position to be absolute which means we can position it everywhere on the page. In this case, we just put it in the top left corner (top: 0px and left: 0px) but since it has 100% width and height it will cover the whole screen.

To make sure that it is really on top of all other elements, we set z-index to a high value (by default all elements have a z-index value of 0).

Finally, we get the transparent grey effect by setting the background color to be black but only at 30% opacity. #000 or #000000 or rgb(0,0,0) represent the black color and we can make it 30% transparent by setting the type to rgba (notice the a) and 30% is represented as 0.3.

Now, we can do almost the same thing with the modal window!

<style>
  .modal {
    width: 300px;
    height: 300px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -150px;
    margin-left: -150px;
    z-index: 9999;
    background: yellow;
  }
</style>

<div class="modal"></div>

In this case, our modal window size is going to be 300x300. We again position it absolutely but this time set the top left corner of our modal window to be in the middle of the screen by using (top: 50% and left: 50%). To center it properly, we can move it to the left a little bit with margin-left of half its size -150px, same with margin-top. Then we just make sure it's on top of everything else (z-index: 9999) including the backdrop (z-index: 9998). The yellow background is there to help us see the window.

Now, what if we don't want to set a fixed size for our modal window?

There is some CSS3 magic that we can do.

.modal {
  position: absolute,
  top: 50%,
  left: 50%,
  transform: translate(-50%, -50%),
  z-index: 9999,
  background: yellow;
}

This time, instead of specifying width and height and correcting the position with negative margin-left and margin-top, we are using tranform: translate(x, y) which understands percentages and will calculate the necessary size for us. The translate transformation moves an element from its current position by x pixels to the right and y pixels down.

Knowing all that, our basic modal component will look like this.

class Modal extends React.Component {
  render() {
    if (this.props.isOpen === false)
      return null

    let modalStyle = {
      position: 'absolute',
      top: '50%',
      left: '50%',
      transform: 'translate(-50%, -50%)',
      zIndex: '9999',
      background: '#fff'
    }

    let backdropStyle = {
      position: 'absolute',
      width: '100%',
      height: '100%',
      top: '0px',
      left: '0px',
      zIndex: '9998',
      background: 'rgba(0, 0, 0, 0.3)'
    }

    return (
      <div>
        <div style={modalStyle}>{this.props.children}</div>
        <div style={backdropStyle} onClick={e => this.close(e)}/>}
      </div>
    )
  }

  close(e) {
    e.preventDefault()

    if (this.props.onClose) {
      this.props.onClose()
    }
  }
}

If isOpen property is true or not set, then we will render the contents of the modal window. Otherwise, if isOpen is explicitly set to false, we render nothing and the modal is not visible.

Next, we transfer our style to a JavaScript object literal. Notice that z-index becomes zIndex.

Finally, we render our divs with the appropriate styles and when the user clicks on the backdrop we raise the onClose event that our application state component can listen to.

We can make our modal component more fancy by letting the user optionally specify width and height

if (this.props.width && this.props.height) {
  modalStyle.width = this.props.width + 'px'
  modalStyle.height = this.props.height + 'px'
  modalStyle.marginLeft = '-' + (this.props.width/2) + 'px',
  modalStyle.marginTop = '-' + (this.props.height/2) + 'px',
  modalStyle.transform = null
}

or override modal style

if (this.props.style) {
  for (let key in this.props.style) {
    modalStyle[key] = this.props.style[key]
  }
}

and/or provide custom class names for additional styling using separate CSS

return (
  <div className={this.props.containerClassName}>
    <div className={this.props.className} style={modalStyle}>
      {this.props.children}
    </div>
    {!this.props.noBackdrop &&
        <div className={this.props.backdropClassName} style={backdropStyle}
             onClick={e => this.close(e)}/>}
  </div>
)

Final result

Here is the complete example.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>React Modal Demo</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react-dom.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
</head>
<body>
  <div id="app"></div>

  <script type="text/jsx">
  class App extends React.Component {
    constructor(props) {
      super(props)
      this.state = { isModalOpen: false }
    }

    render() {
      return (
        <div>
          <button onClick={() => this.openModal()}>Open modal</button>
          <Modal isOpen={this.state.isModalOpen} onClose={() => this.closeModal()}>
            <h1>Modal title</h1>
            <p>hello</p>
            <p><button onClick={() => this.closeModal()}>Close</button></p>
          </Modal>
        </div>
      )
    }

    openModal() {
      this.setState({ isModalOpen: true })
    }

    closeModal() {
      this.setState({ isModalOpen: false })
    }
  }

  class Modal extends React.Component {
    render() {
      if (this.props.isOpen === false)
        return null

      let modalStyle = {
        position: 'absolute',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
        zIndex: '9999',
        background: '#fff'
      }

      if (this.props.width && this.props.height) {
        modalStyle.width = this.props.width + 'px'
        modalStyle.height = this.props.height + 'px'
        modalStyle.marginLeft = '-' + (this.props.width/2) + 'px',
        modalStyle.marginTop = '-' + (this.props.height/2) + 'px',
        modalStyle.transform = null
      }

      if (this.props.style) {
        for (let key in this.props.style) {
          modalStyle[key] = this.props.style[key]
        }
      }

      let backdropStyle = {
        position: 'absolute',
        width: '100%',
        height: '100%',
        top: '0px',
        left: '0px',
        zIndex: '9998',
        background: 'rgba(0, 0, 0, 0.3)'
      }

      if (this.props.backdropStyle) {
        for (let key in this.props.backdropStyle) {
          backdropStyle[key] = this.props.backdropStyle[key]
        }
      }

      return (
        <div className={this.props.containerClassName}>
          <div className={this.props.className} style={modalStyle}>
            {this.props.children}
          </div>
          {!this.props.noBackdrop &&
              <div className={this.props.backdropClassName} style={backdropStyle}
                   onClick={e => this.close(e)}/>}
        </div>
      )
    }

    close(e) {
      e.preventDefault()

      if (this.props.onClose) {
        this.props.onClose()
      }
    }
  }

  ReactDOM.render(<App/>, document.getElementById('app'))
  </script>
</body>
</html>

Other approaches