Mark
Mark

Reputation: 783

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>
    </>
  );
}

Upvotes: 0

Views: 913

Answers (2)

samran feli
samran feli

Reputation: 342

SidePanelLayer.js:

import { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { ChevronLeftIcon } from "@heroicons/react/20/solid";

import { menuItems, about } from "../data/menu";

export default function SlidePanelLayer({ activeNavbar, setActiveNavbar }) {
  //  const [open, setOpen] = useState(true);

  const activeSubmenu = menuItems.find(item => item.title === activeNavbar);

  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">
                        {activeSubmenu?.title}
                      </div>
                      <hr className="my-6" />
                    </div>
                    <div className="relative flex-1 px-4 sm:px-6">
                      {activeSubmenu?.submenu?.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>
  );
}

menu.js:

import {
  PhoneIcon,
  PlayCircleIcon,
  RectangleGroupIcon,
  ChevronRightIcon,
  ChatBubbleLeftIcon,
  EnvelopeIcon,
  MapPinIcon,
} from "@heroicons/react/20/solid";

export const about = [
  {
    title: "Who we are",
    description:
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur aliquet nisi vel elementum semper.",
    links: [
      {
        title: "Lorum ipsum",
        description:
          "Curabitur eu tincidunt ante, nec dapibus lectus. Morbi dapibus vitae diam eget accumsan.",
        icon: ChevronRightIcon,
        href: "#",
      },
      {
        title: "Dolor sit",
        description:
          "Aliquam eu odio elit. Proin lacus dolor, gravida nec augue et, interdum egestas libero.",
        icon: ChevronRightIcon,
        href: "#",
      },
    ],
  },
  {
    title: "What can we do",
    description:
      "In blandit dapibus tincidunt. Phasellus nisi leo, dapibus in dapibus vitae, varius eu lacus. Mauris dapibus, massa a pretium efficitur.",
    links: [
      {
        title: "Cras eget",
        description:
          "Cras varius, elit sit amet finibus pretium, lorem neque condimentum felis",
        icon: ChevronRightIcon,
        href: "#",
      },
      {
        title: "Nam ex enim",
        description:
          "luctus sit amet magna sit amet, condimentum congue justo. Nunc quis velit sapien.",
        icon: ChevronRightIcon,
        href: "#",
      },
    ],
  },
];

export const projects = [
  {
    title: "Our projects",
    description:
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur aliquet nisi vel elementum semper.",
    links: [
      {
        title: "Lorum ipsum",
        description:
          "Curabitur eu tincidunt ante, nec dapibus lectus. Morbi dapibus vitae diam eget accumsan.",
        icon: ChevronRightIcon,
        href: "#",
      },
      {
        title: "Dolor sit",
        description:
          "Aliquam eu odio elit. Proin lacus dolor, gravida nec augue et, interdum egestas libero.",
        icon: ChevronRightIcon,
        href: "#",
      },
    ],
  },
  {
    title: "Latest projects",
    description:
      "In blandit dapibus tincidunt. Phasellus nisi leo, dapibus in dapibus vitae, varius eu lacus. Mauris dapibus, massa a pretium efficitur.",
    links: [
      {
        title: "Cras eget",
        description:
          "Cras varius, elit sit amet finibus pretium, lorem neque condimentum felis",
        icon: ChevronRightIcon,
        href: "#",
      },
      {
        title: "Nam ex enim",
        description:
          "luctus sit amet magna sit amet, condimentum congue justo. Nunc quis velit sapien.",
        icon: ChevronRightIcon,
        href: "#",
      },
    ],
  },
];

export const menuItems = [
  {
    title: "About",
    href: "#",
    submenu: about
  },
  {
    title: "Projects",
    href: "#",
    submenu:projects
  },
  {
    title: "Services",
    href: "#",
  },
  {
    title: "Contact",
    href: "#",
  },
];

Upvotes: 1

samran feli
samran feli

Reputation: 342

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>
  );
}

Upvotes: 1

Related Questions