Building a Responsive Navbar with Qwik

In this tutorial, we'll walk through the process of creating a responsive navigation bar (navbar) using Qwik, a performance-first web framework. Our navbar will include a hamburger menu for mobile devices and a horizontal menu for larger screens.

Importing Necessary Dependencies:

For our navbar, we'll need several utilities and components from Qwik. Import them at the beginning of your component file:


import {
  component$,
  type Signal,
  useStyles$,
  useOnDocument,
  $,
} from "@builder.io/qwik";
import styles from "./header.module.css";
import animationStyles from "./styles.css?inline";
import { Image } from "@unpic/qwik";
import {
  Link,
  useNavigate,
  useContent,
  useLocation,
  type ContentMenu,
} from "@builder.io/qwik-city";
import { useSignal, useStore } from "@builder.io/qwik";
import { useCSSTransition, Stage } from "qwik-transition";

Create the type that will be passed to the MobileMenu component.

type MobileMenuProps = {
  stage: Signal<Stage>;
  menu: ContentMenu;
  url: URL;
};

Defining the Navbar Component:

Our main navbar component will contain a logo, a button to toggle the mobile menu, and a horizontal menu for larger screens.


export default component$(() => {
useStyles$(animationStyles);
const isMenuOpen = useSignal<boolean>(false);
const { menu } = useContent();
const { url } = useLocation();
const { stage } = useCSSTransition(isMenuOpen, {
    timeout: 500,
    transitionOnAppear: false,
  });
  return (
    <>
      <header class={styles.header}>
        // ... (rest of the code)
      </header>
      <MobileMenu stage={stage} menu={menu as ContentMenu} url={url} />
    </>
  );
});

Mobile Menu Toggle:

For mobile devices, we'll use a hamburger icon that, when clicked, will toggle the visibility of the mobile menu. We'll use Qwik's useSignal to manage the state of the menu (open or closed).

const isMenuOpen = useSignal<boolean>(false);
<button
   onClick$={() => {
     isMenuOpen.value = !isMenuOpen.value;
      }}
   aria-label="Toggle Menu"  
>

The SVG icon will change its appearance based on the state of the isMenuOpen signal.

  <svg
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 24 24"
  class={`inline-block w-12 h-12 hamburger ${
   stage.value === "enterTo"
    ? "btn-entered"
     : "btn-animating"
      }`}
    >

We are using 'qwik-transition' an easy-to-use custom Qwik hook for adding smooth animations and effects to your Qwik components. You can find the package here.

  const { stage } = useCSSTransition(isMenuOpen, {
    timeout: 500,
    transitionOnAppear: false,
  });

The 'stage' value will change when the 'isMenuOpen' Signal value changes.

      <div
        class={`mobile-menu bg-primary fixed top-0 h-full z-50 flex justify-center ${
          stage.value === "enterTo" ? "menu-entered" : "menu-animating"
        }`}
      >

Swipe Gesture for Mobile:

To enhance the user experience on mobile devices, we've added a feature to close the menu when the user swipes left. This is achieved using the touchstart and touchend event listeners.


Qwik provides a few hooks for listening to window/dom events. They are:

  • useOn(): listen to events on the current component's root element.
  • useOnWindow(): listen to events on the window object.
  • useOnDocument(): listen to events on the document object.

There are two advantages to this:

  • The events can be registered declaratively in your JSX.
  • The events get automatically cleaned up when the component is destroyed (No explicit bookkeeping and cleanup is needed).
 const touchStartX = useStore({ x: 0 });

  // Listen for the touchstart event and store the initial touch position
  useOnDocument(
    "touchstart",
    $((event: Event) => {
      const touchEvent = event as TouchEvent;
      touchStartX.x = touchEvent.touches[0].clientX;
    })
  );

  // Listen for the touchend event, compare touch positions, and close the menu if a leftward swipe is detected
  useOnDocument(
    "touchend",
    $((event: Event) => {
      const touchEvent = event as TouchEvent;
      const touchEndX = touchEvent.changedTouches[0].clientX;
      const threshold = 5; // Adjust this value as needed

      // Detect a leftward swipe
      if (touchStartX.x - touchEndX > threshold) {
        isMenuOpen.value = false;
      }
    })
  );

Displaying Menu Items:

To display the menu items, we'll use the useContent hook from Qwik, which allows us to fetch content (like our menu structure) and render it.


const { menu } = useContent();

We then map over the menu.items to render each menu item.

{menu
  ? menu.items?.map((item) => (
       <>
           <ul>
                {item.items?.map((item) => (
                    <li>
                   <Link
                       href={item.href}
                       class={{
                       active: url.pathname === item.href,
                         }}
                         >
                     {item.text}
                  </Link>
                </li>
               ))}
         </ul>
       </>
        ))
: null}

You will need to create a 'menu.md' file that contains the menu's structure for the directory its in. The 'useContent()' function is used to retreive the menu structure in a template for rendering. You can read more about it in the official docs here. If this is your main navigation, you'll most likely put it in the root directory.

Mobile Menu Component:

The MobileMenu component is responsible for rendering the menu on mobile devices. It takes in the stage (to handle animations), the menu content, and the current url to highlight the active menu item.


export const MobileMenu = component$<MobileMenuProps>(
  ({ stage, menu, url }) => {
    const nav = useNavigate();
    useStyles$(animationStyles);

    return (
      <div
        tabIndex={-1} // Make the div focusable
        class={`mobile-menu bg-primary fixed top-0 h-full z-50 flex justify-center ${
          stage.value === "enterTo" ? "menu-entered" : "menu-animating"
        }`}
      >
        {menu
          ? menu.items?.map((item) => (
              <>
                <ul class="p-5">
                  {item.items?.map((item) => (
                    <li>
                      <button
                        disabled={url.pathname === item.href}
                        class={`btn btn-primary w-[175px] ${
                          url.pathname === item.href && "mobileActive" // Add the active class if the current path matches the menu item's href
                        }`}
                        onClick$={() => nav(item.href)}
                      >
                        {item.text}
                      </button>
                    </li>
                  ))}
                </ul>
              </>
            ))
          : null}
      </div>
    );
  }
);

Conclusion

Building a responsive navbar with Qwik is straightforward and efficient. By leveraging Qwik's utilities and hooks, we can create interactive and performant components with ease. Whether you're building a simple website or a complex web app, Qwik provides the tools to make your development process smooth and enjoyable.