Search code examples
reactjsnext.jsnext.js13next.js14

Is there a way to override layouts in NextJS 14?


I have what I would guess is a pretty common setup in NextJS 13/14 app router. I have a layout.tsx file which shows my sticky navbar at the top of the screen for navigation purposes:

import "./globals.css";
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <div className="relative">
          <!-- My navbar is here --> 
          <div className="sticky top-0 z-50 h-16 w-full bg-blue-200" />;
          <div className="px-4">
            <div className="flex-grow p-6 md:overflow-y-auto ">{children}</div>
          </div>
        </div>
      </body>
    </html>
  );
}

And then I create my page.tsx file, which has a modal in it:

import Modal from "./Modal";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-4">
      <div className="mx-auto">
        <p>Hello world!</p>
        <div className="w-full h-full">
          <Modal />
        </div>
      </div>
    </main>
  );
}

Here is the Modal code:

"use client";

import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";

export default function Modal() {
  const [isOpen, setIsOpen] = useState(false);

  function toggleOpen() {
    setIsOpen(!isOpen);
  }

  return (
    <>
      <button
        className=" border-2 rounded-lg border-gray-400"
        onClick={toggleOpen}
      >
        Open me!
      </button>
      <Transition appear show={isOpen} as={Fragment}>
        <Dialog as="div" className="relative z-10" onClose={toggleOpen}>
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black/25" />
          </Transition.Child>

          <div className="fixed inset-0 overflow-y-auto">
            <div className="flex min-h-full items-center justify-center p-4 text-center">
              <Dialog.Panel className="text-center w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 align-middle shadow-xl transition-all">
                <Dialog.Title
                  as="h3"
                  className={`text-black text-lg font-medium leading-6`}
                >
                  {"This is a header"}
                </Dialog.Title>

                <div className="flex mt-4 text-center w-full  justify-center">
                  <div className="ml-3">
                    <button className="rounded-md" onClick={toggleOpen}>
                      Close modal
                    </button>
                  </div>
                </div>
              </Dialog.Panel>
            </div>
          </div>
        </Dialog>
      </Transition>
    </>
  );
}

Which gives us something like this:

example-site

And when I open the modal, something uglier:

example-site-open-modal

My issue is that when I open the dialog, the modal transition does NOT obscure layout.tsx (e.g. the blue header), but it obscures the rest of the page not defined by the layout component (e.g. the children of the layout). I know this is because of the way that Nextjs nests components: NextJS component nesting

So I suppose my question is, isn't the fact that any full-screen overlay doesn't go over top of the code in layout.tsx a serious shortcoming of using layout.tsx for any sort of navigation/permanent fixture purposes? If the layout will always sit on top of any modal overlay, won't that always just be pretty ugly, no matter what?


Solution

  • I think this can just be solved with css as opposed to needing to do anything funky with next's project structure. It looks like the sticky header has a z-index of 50, so your transition component needs to have a higher z-index to show up on top.

    Additionally, the sticky header's width issue is probably being caused by a scrollbar appearing/disappearing after disabling scrolling when the modal is opened up? In that case you can just set the header's width to 100vw instead of 100%.