Here’s some code to create a centered modal that will work even when the content within the modal is larger than the screen. I’ll be using TailwindCSS, React, and Next.js, but you don’t have to.

Live demo: nomadz.app

Untitled

Requirements

Most sample code for modals breaks when the content within the modal is vertically larger than the screen and requires scrolling. This will happen when you use the typical flexbox or transform: translate(-50%, -50%) centering. We also need to make sure the close button remains fixed to the top of the modal even when scrolling within the modal.

Simple Flex Centering (this won’t work if modal content overflows)

const Modal = ({ onClose }) => {
  return (
    <>
      <div
        className="fixed inset-0 w-full h-full flex items-center justify-center"
        style={{ backgroundColor: "rgba(0,0,0,0.4)" }}
        onClick={onClose}
      >
        <div
          className="bg-gray-800 max-w-md mx-auto rounded px-6 py-3"
          onClick={(event) => event.stopPropagation()}
        >
          <h3>Some Heading</h3>
          <div>Text</div>
          <div>Sample content</div>
          <div>Sample content</div>
        </div>
      </div>
    </>
  );
};

The solution to enabling scrolling within modals is to have both a container and an inner div, where the inner div has an overflow-y: auto (or scroll) and a max-height relative to the viewport (eg. 80vh), and the container is margin centered horizontally, transform centered vertically.

import CloseIcon from "src/icons/CloseIcon.svg";

const Modal = ({ children, onClose, closeOnOutsideClick }) => {
  return (
		<div
      className="fixed inset-0 z-1050"
      onClick={closeOnOutsideClick ? onClose : null}
    >
      <div
        className="modal-container"
        onClick={(event) => event.stopPropagation()}
      >
        <div className="flex justify-end p-4">
          <button
            type="button"
            onClick={onClose}
            className="hover:text-gray-900"
          >
            <CloseIcon />
          </button>
        </div>
        <div className="modal-content-inner px-8 pt-0 pb-12">{children}</div>
      </div>
    </div>
  );
};

export default Modal;

The closeOnOutsideClick prop being true just means that clicking outside the modal with close it.

CSS

.modal-container {
  max-height: 85%;
  width: 90%;
  margin-left: auto;
  margin-right: auto;
  overflow: hidden;
  
  position: relative;
  top: 50%;
  transform: translateY(-50%);

  @apply rounded-lg shadow bg-white dark:bg-gray-700;
}

@media (min-width: 768px) {
  .modal-container {
    max-width: 600px;
  }
}

.modal-content-inner {
  overflow-y: auto;
  max-height: 80vh;
}

This is the only way to center (that I’m aware of) that will still work even when the content within the modal is larger than the page height. Other methods of centering (eg. flexbox) will fail in that scenario. When on mobile, the modal’s width will be 90% of the screen width.

Here’s a sample page in Next.js that shows how the modal looks when small and when expanded.

import { useState } from "react";
import type { NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import Modal from "src/modules/modal/components/Modal";

const ModalContent = () => {
  const [expanded, setExpanded] = useState(false);
  return (
    <div className="max-w-lg space-y-3">
      <p>
        As a digital nomad for the last 5 years, I've preferred working outside
        of my living space in nice cafes, parks, etc. Finding the right place to
        work can make all the difference in terms of happiness and productivity.
      </p>
      <p>
        Of course coworking spaces are easy to find, but finding nice free/cheap
        places to work (outside of Korea) is surprisingly difficult.
      </p>
      <button className="btn" onClick={() => setExpanded(!expanded)}>
        Click
      </button>
      {!expanded&& (
        <>
          <p>
            I built this site to serve as a resource for people to find and
            share cool free places to work (or at most for the price of a cup of
            coffee).
          </p>
          <p>
            Although for now it's just places to work, in the future it'll
            support any type of location useful for nomads (eg. free outdoor
            gyms, coliving spaces).
          </p>
          <div className="bg-red-200 w-full h-64"></div>
          <div className="bg-blue-200 w-full h-64"></div>
          <div className="bg-orange-200 w-full h-64"></div>
          <p>
            You can check out some of my personal favorite places to work{" "}
            <Link href="/?profile_id=219371b8-fe8e-4823-95ff-0002b471b87d">
              <a>here</a>
            </Link>
            . The country with by far the highest abundance of nice cafes to
            work out of (as well as free outdoor gyms) that I've been is South
            Korea (this site would be useless for Korea because you're always
            less than a block away from a nice work cafe). Cities with some of
            my favorite places to work outside of Seoul include Belgrade, Kyiv,
            London, and Paris.
          </p>
          <p>
            Although for now it's just places to work, in the future it'll
            support any type of location useful for nomads (eg. free outdoor
            gyms, coliving spaces).
          </p>
          <p className="text-sm">
            I built this site with Next.js and TypeORM/TypeGraphQL. Send me any
            feedback{" "}
            <Link href="<https://www.twitter.com/jeremybernier>">
              <a>@jeremybernier</a>
            </Link>{" "}
            or find my email on my{" "}
            <Link href="<https://www.jbernier.com>">
              <a>personal site</a>
            </Link>
          </p>
        </>
      )}
    </div>
  );
};

const RandomPage: NextPage = () => {
  const [showModal, setShowModal] = useState(true);

  return (
    <div>
      <Head>
        <title>Page Title</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className="max-w-4xl mx-auto">
        <button
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
          onClick={() => setShowModal(!showModal)}
        >
          Toggle Modal
        </button>
        {showModal && (
          <Modal onClose={() => setShowModal(false)}>
            <ModalContent />
          </Modal>
        )}
      </main>
    </div>
  );
};

export default RandomPage;

And here’s the CloseIcon SVG icon

<svg xmlns="<http://www.w3.org/2000/svg>" xmlns:xlink="<http://www.w3.org/1999/xlink>" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6Z"></path></svg>