Search code examples
javascriptreactjsnext.jstailwind-cssheadless-ui

How to create a slide menu with React and Headless UI (Tailwind)


I am trying to create a slide-over navbar or slide menu with panels that open on top of eachother (I haven't found the best description yet as how to describe it).

Basically the idea is to have a slide menu that has sub-menu items that slide in (on top of eachother) as well. When a sub-menu item has been opened have the 'Back' button to go back to the main menu.

Here's a link with full code example: https://codesandbox.io/p/sandbox/over-lay-menu-n92xys

If you take a look at my SlidePanel.js you can see how I am trying to pass props to SlidePanelLayer.js hoping this would achieve that but in practice it does not work well:

"use client";

import { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import { menuItems } from "../data/menu";

import SlidePanelLayer from "./SlidePanelLayer";

export default function SlidePanel({ slideOpen, setSlideOpen }) {
  const [openLayer, setOpenLayer] = useState(false);

  return (
    <>
      <SlidePanelLayer
        openLayer={openLayer}
        setOpenLayer={setOpenLayer}
        setSlideOpen={setSlideOpen}
      />
      <Transition.Root show={slideOpen} as={Fragment}>
        <Dialog as="div" className="relative z-50" onClose={setSlideOpen}>
          <Transition.Child
            as={Fragment}
            enter="ease-in-out duration-500"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in-out duration-500"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
          </Transition.Child>

          <div className="fixed inset-0 overflow-hidden">
            <div className="absolute inset-0 overflow-hidden">
              <div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
                <Transition.Child
                  as={Fragment}
                  enter="transform transition ease-in-out duration-500 sm:duration-700"
                  enterFrom="translate-x-full"
                  enterTo="translate-x-0"
                  leave="transform transition ease-in-out duration-500 sm:duration-700"
                  leaveFrom="translate-x-0"
                  leaveTo="translate-x-full"
                >
                  <Dialog.Panel className="pointer-events-auto w-screen max-w-md">
                    <div className="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl rounded-l-2xl">
                      <div className="px-4 sm:px-6">
                        <div className="flex items-start justify-between">
                          <div className="flex h-7 items-center">
                            <button
                              type="button"
                              className="flex gap-2 items-center relative rounded-md bg-white text-gray-400 hover:text-gray-500"
                              onClick={() => setSlideOpen(false)}
                            >
                              <span className="sr-only">Close</span>
                              <XMarkIcon
                                className="h-5 w-5 text-secondary-90"
                                aria-hidden="true"
                              />
                              <span className="text-gray-900 font-bold">
                                Close
                              </span>
                            </button>
                          </div>
                        </div>
                      </div>
                      <div className="relative mt-6 flex-1 px-4 sm:px-6">
                        {/* Start content */}

                        <div className="mt-6 flow-root">
                          <div className="-my-6">
                            <div className="space-y-2 pt-6 pb-4">
                              {menuItems.map((item) => (
                                <a
                                  key={item.title}
                                  href={item.href}
                                  onClick={() => setOpenLayer(true)}
                                  className="-mx-3 block rounded-lg px-3 py-2 text-base leading-7 text-secondary-90 hover:bg-gray-50 font-bold"
                                >
                                  {item.title}
                                </a>
                              ))}
                            </div>
                          </div>
                        </div>

                        {/* End content */}
                      </div>
                    </div>
                  </Dialog.Panel>
                </Transition.Child>
              </div>
            </div>
          </div>
        </Dialog>
      </Transition.Root>
    </>
  );
}

Solution

  • Header.js:

    "use client";
    
    import { useState } from "react";
    
    import { Bars3Icon } from "@heroicons/react/24/outline";
    
    import SlidePanel from "@/components/SlidePanel";
    
    export default function HeaderComponent() {
      const [activeNavbar, setActiveNavbar] = useState("");
      return (
        <header className="relative isolate z-40">
          <div className="flex lg:flex-1 justify-between items-center  mx-auto max-w-7xl px-6 lg:px-8 py-4 lg:py-8">
            <a href="/" className="-m-1.5 p-1.5">
              <span className="sr-only">Over-lay</span>
              <span>Over-lay menu</span>
            </a>
    
            <div className="flex">
              <button
                type="button"
                className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700"
                onClick={() => setActiveNavbar("mainMenu")}
              >
                <span className="sr-only">Open menu</span>
                <Bars3Icon className="h-6 w-6" aria-hidden="true" />
              </button>
            </div>
          </div>
    
          <SlidePanel
            activeNavbar={activeNavbar}
            setActiveNavbar={setActiveNavbar}
          />
        </header>
      );
    }
    

    SlidePannel.js:

    "use client";
    
    import { Fragment, useState } from "react";
    import { Dialog, Transition } from "@headlessui/react";
    import { XMarkIcon } from "@heroicons/react/24/outline";
    import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
    import { menuItems } from "../data/menu";
    
    import SlidePanelLayer from "./SlidePanelLayer";
    
    export default function SlidePanel({ activeNavbar, setActiveNavbar }) {
      // const [openLayer, setOpenLayer] = useState(false);
      return (
        <>
          <SlidePanelLayer
            activeNavbar={activeNavbar}
            setActiveNavbar={setActiveNavbar}
          />
          <Transition.Root show={!!activeNavbar} as={Fragment}>
            <Dialog
              as="div"
              className="relative z-50"
              onClose={() => setActiveNavbar("")}
            >
              <Transition.Child
                as={Fragment}
                enter="ease-in-out duration-500"
                enterFrom="opacity-0"
                enterTo="opacity-100"
                leave="ease-in-out duration-500"
                leaveFrom="opacity-100"
                leaveTo="opacity-0"
              >
                <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
              </Transition.Child>
    
              <div className="fixed inset-0 overflow-hidden">
                <div className="absolute inset-0 overflow-hidden">
                  <div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
                    <Transition.Child
                      as={Fragment}
                      enter="transform transition ease-in-out duration-500 sm:duration-700"
                      enterFrom="translate-x-full"
                      enterTo="translate-x-0"
                      leave="transform transition ease-in-out duration-500 sm:duration-700"
                      leaveFrom="translate-x-0"
                      leaveTo="translate-x-full"
                    >
                      <Dialog.Panel className="pointer-events-auto w-screen max-w-md">
                        <div className="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl rounded-l-2xl">
                          <div className="px-4 sm:px-6">
                            <div className="flex items-start justify-between">
                              <div className="flex h-7 items-center">
                                <button
                                  type="button"
                                  className="flex gap-2 items-center relative rounded-md bg-white text-gray-400 hover:text-gray-500"
                                  onClick={() => setActiveNavbar("")}
                                >
                                  <span className="sr-only">Close</span>
                                  <XMarkIcon
                                    className="h-5 w-5 text-secondary-90"
                                    aria-hidden="true"
                                  />
                                  <span className="text-gray-900 font-bold">
                                    Close
                                  </span>
                                </button>
                              </div>
                            </div>
                          </div>
                          <div className="relative mt-6 flex-1 px-4 sm:px-6">
                            {/* Start content */}
    
                            <div className="mt-6 flow-root">
                              <div className="-my-6">
                                <div className="space-y-2 pt-6 pb-4">
                                  {menuItems.map((item) => (
                                    <a
                                      key={item.title}
                                      href={item.href}
                                      onClick={() => setActiveNavbar(item.title)}
                                      className="-mx-3 block rounded-lg px-3 py-2 text-base leading-7 text-secondary-90 hover:bg-gray-50 font-bold"
                                    >
                                      {item.title}
                                    </a>
                                  ))}
                                </div>
                              </div>
                            </div>
    
                            {/* End content */}
                          </div>
                        </div>
                      </Dialog.Panel>
                    </Transition.Child>
                  </div>
                </div>
              </div>
            </Dialog>
          </Transition.Root>
        </>
      );
    }
    

    SlidePannelLayer.js:

    import { Fragment, useState } from "react";
    import { Dialog, Transition } from "@headlessui/react";
    import { ChevronLeftIcon } from "@heroicons/react/20/solid";
    
    import { about } from "../data/menu";
    
    export default function SlidePanelLayer({ activeNavbar, setActiveNavbar }) {
      //  const [open, setOpen] = useState(true);
      return (
        <Transition.Root
          show={!!activeNavbar && activeNavbar !== "mainMenu"}
          as={Fragment}
        >
          <Dialog
            as="div"
            className="relative z-50"
            onClose={() => {
              setActiveNavbar("mainMenu");
            }}
          >
            {/* <Transition.Child
              as={Fragment}
              enter="ease-in-out duration-500"
              enterFrom="opacity-0"
              enterTo="opacity-100"
              leave="ease-in-out duration-500"
              leaveFrom="opacity-100"
              leaveTo="opacity-0"
            >
              <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
            </Transition.Child> */}
    
            <div className="fixed inset-0 overflow-hidden">
              <div className="absolute inset-0 overflow-hidden">
                <div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
                  <Transition.Child
                    as={Fragment}
                    enter="transform transition ease-in-out duration-500 sm:duration-700"
                    enterFrom="translate-x-full"
                    enterTo="translate-x-0"
                    leave="transform transition ease-in-out duration-500 sm:duration-700"
                    leaveFrom="translate-x-0"
                    leaveTo="translate-x-full"
                  >
                    <Dialog.Panel className="pointer-events-auto w-screen max-w-md">
                      <div className="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl rounded-l-2xl">
                        <div className="px-4 sm:px-6">
                          <div className="flex items-start justify-between">
                            <div className="flex h-7 items-center">
                              <button
                                type="button"
                                className="flex gap-2 items-center relative rounded-md bg-white text-gray-400 hover:text-gray-500"
                                onClick={() => setActiveNavbar("mainMenu")}
                              >
                                <span className="sr-only">Back</span>
                                <ChevronLeftIcon
                                  className="h-5 w-5 text-secondary-90"
                                  aria-hidden="true"
                                />
                                <span className="text-gray-900 font-bold">
                                  Back
                                </span>
                              </button>
                            </div>
                          </div>
                          <div className="text-secondary-90 font-bold mt-6">
                            About
                          </div>
                          <hr className="my-6" />
                        </div>
                        <div className="relative flex-1 px-4 sm:px-6">
                          {about.map((item) => (
                            <div
                              key={item.title}
                              className="relative pb-12 leading-6"
                            >
                              <h2 className="mt-1 text-secondary-90 text-2xl font-bold">
                                {item.title}
                              </h2>
                              <p className="mt-1 mb-6 text-secondary-90">
                                {item.description}
                              </p>
                              {item.links.map((link) => (
                                <a
                                  key={link.title}
                                  href={link.href}
                                  className="mt-4 block text-secondary-90 hover:text-secondary-100 group"
                                >
                                  <span className="flex items-center">
                                    {link.icon && (
                                      <link.icon
                                        className="flex-none w-5 h-5 text-gray-400"
                                        aria-hidden="true"
                                      />
                                    )}
                                    <span className="ml-2 font-bold group-hover:underline">
                                      {link.title}
                                    </span>
                                  </span>
                                  <span className="block text-sm">
                                    {link.description}
                                  </span>
                                </a>
                              ))}
                            </div>
                          ))}
                        </div>
                      </div>
                    </Dialog.Panel>
                  </Transition.Child>
                </div>
              </div>
            </div>
          </Dialog>
        </Transition.Root>
      );
    }