NavigationIntermediateApril 9, 2026

Nav Menu Panel

Fixed header bar with a centered menu toggle that expands a full-width slide-down panel. Panel contains large flip-text nav links, contact info, and image cards. CSS grid accordion for the open/close animation, flip-text hover effect on links, green accent square on active/hover state.

View Full Demo →
demo.jsx
import NavMenuPanel from "./index.jsx";
import styles from "./demo.module.css";

export default function NavMenuPanelDemo() {
  return (
    <div className={styles.demo}>
      <NavMenuPanel />
      <div className={styles.hero}>
        <p className={styles.label}>Creative Studio — 2024</p>
        <h1 className={styles.heading}>
          Building<br />Experiences
        </h1>
        <p className={styles.sub}>Web Design &amp; Development</p>
      </div>
      <div className={styles.footer}>
        <span>scroll to explore</span>
        <span>↓</span>
      </div>
    </div>
  );
}
index.jsx
"use client";

import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import styles from "./styles.module.css";

const DEFAULT_NAV_LINKS = [
  { label: "Home", href: "/" },
  { label: "Work", href: "/work" },
  { label: "Pricing", href: "/pricing" },
  { label: "About", href: "/about" },
  { label: "Contact", href: "/contact" },
];

const DEFAULT_CONTACT_INFO = {
  general: "contact@No-5.studio",
  socials: [
    { label: "LinkedIn", handle: "@No-5.studio" },
    { label: "Github", handle: "@kevindaviis" },
  ],
  notices: ["Accepting projects."],
};

const DEFAULT_IMAGE_CARDS = [
  { src: "/demo-assets/models/model0.png", caption: "About the Studio" },
  { src: "/demo-assets/models/model1.png", caption: "Featured Project" },
];

export default function NavMenuPanel({
  navLinks = DEFAULT_NAV_LINKS,
  contactInfo = DEFAULT_CONTACT_INFO,
  imageCards = DEFAULT_IMAGE_CARDS,
}) {
  const [isOpen, setIsOpen] = useState(false);
  const pathname = usePathname();

  useEffect(() => {
    const handleKey = (e) => {
      if (e.key === "Escape" && isOpen) setIsOpen(false);
    };
    document.addEventListener("keydown", handleKey);
    return () => document.removeEventListener("keydown", handleKey);
  }, [isOpen]);

  useEffect(() => {
    document.body.style.overflow = isOpen ? "hidden" : "";
    return () => { document.body.style.overflow = ""; };
  }, [isOpen]);

  return (
    <div className={styles.root}>
      <div
        className={`${styles.overlay} ${isOpen ? styles.overlayVisible : ""}`}
        onClick={() => setIsOpen(false)}
        aria-hidden="true"
      />

      <div className={styles.navSection}>
        <header className={styles.header}>

          {/* ── Bar ── */}
          <div className={styles.bar}>
            <Link href="/" className={styles.logo}>N°5</Link>
            <button
              className={styles.menuToggle}
              onClick={() => setIsOpen(!isOpen)}
              aria-label={isOpen ? "Close menu" : "Open menu"}
            >
              {isOpen ? (
                <>
                  <span className={styles.toggleLabel}>Close</span>
                  <span className={styles.toggleIcon}>✕</span>
                </>
              ) : (
                <>
                  <span className={styles.toggleLabel}>Menu</span>
                  <span className={styles.toggleIcon}>≡</span>
                </>
              )}
            </button>
            <div className={styles.ctaWrapper}>
              <a href="/contact" className={styles.ctaLabel}>
                <span className={styles.ctaIconInner}>
                  <span className={styles.ctaIconDefault}>Let&apos;s work together</span>
                  <span className={styles.ctaIconHover}>Let&apos;s work together</span>
                </span>
              </a>
              <span className={styles.ctaIcon}>
                <span className={styles.ctaIconInner}>
                  <span className={styles.ctaIconDefault}>+</span>
                  <span className={styles.ctaIconHover}>+</span>
                </span>
              </span>
            </div>
          </div>

          {/* ── Panel ── */}
          <div
            className={`${styles.panel} ${isOpen ? styles.panelOpen : ""}`}
            aria-hidden={!isOpen}
          >
            <div className={styles.panelClip}>
              <div className={styles.panelInner}>

                {/* Nav links */}
                <nav className={styles.navCol}>
                  <ul className={styles.navList}>
                    {navLinks.map((link, i) => {
                      const isActive = pathname === link.href;
                      return (
                        <li
                          key={link.href}
                          className={styles.navItem}
                          style={{ transitionDelay: `${i * 0.05}s` }}
                        >
                          <Link
                            href={link.href}
                            className={`${styles.navLink} ${isActive ? styles.navLinkActive : ""}`}
                            onClick={() => setIsOpen(false)}
                          >
                            <span className={styles.activeSquare} aria-hidden="true" />
                            <span className={styles.navLabelInner}>
                              <span className={styles.navLabelDefault}>{link.label}</span>
                              <span className={styles.navLabelHover}>{link.label}</span>
                            </span>
                          </Link>
                        </li>
                      );
                    })}
                  </ul>
                </nav>

                {/* Contact info */}
                <div className={styles.contactCol}>
                  <p className={styles.contactTagline}>Contact</p>
                  <a href={`mailto:${contactInfo.general}`} className={styles.contactEmail}>
                    {contactInfo.general}
                  </a>
                  <div className={styles.contactSocials}>
                    {contactInfo.socials.map((s) => (
                      <p key={s.label} className={styles.contactSocialItem}>
                        {s.label}: {s.handle}
                      </p>
                    ))}
                  </div>
                  <div className={styles.notices}>
                    <p className={styles.noticesTagline}>Working Globally</p>
                    {contactInfo.notices.map((notice) => (
                      <div key={notice} className={styles.noticeRow}>
                        <span className={styles.noticeSquare} aria-hidden="true" />
                        <p className={styles.noticeText}>{notice}</p>
                      </div>
                    ))}
                  </div>
                </div>

                {/* Image cards */}
                <div className={styles.imageCol}>
                  {imageCards.map((card) => (
                    <div key={card.caption} className={styles.imageCard}>
                      <div className={styles.imageWrap}>
                        <img src={card.src} alt={card.caption} className={styles.cardImg} />
                      </div>
                      <p className={styles.imageCaption}>{card.caption}</p>
                    </div>
                  ))}
                </div>

              </div>
            </div>
          </div>

        </header>
      </div>
    </div>
  );
}
demo.module.css
.demo {
  position: relative;
  min-height: 100vh;
  background: #0e0e0e;
  display: flex;
  flex-direction: column;
  /* Contains fixed-position children (NavMenuPanel) within this element */
  transform: translateZ(0);
}

.hero {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  padding: 8rem 4rem 4rem;
  gap: 1.25rem;
}

.label {
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.35);
}

.heading {
  font-size: clamp(3rem, 8vw, 7rem);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 0.95;
  color: #ffffff;
}

.sub {
  font-size: 1rem;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.4);
}

.footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.5rem 4rem;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.25);
}
styles.module.css
/* Local tokens — component is self-contained */
.root {
  --z-nav: 1000;
  --color-white: #ffffff;
  --color-gray: rgba(255, 255, 255, 0.45);
  --weight-large: 600;
  --font-body: 1rem;
  --font-body-sm: 0.875rem;
  --font-body-2xl: 1.5rem;
  --font-tagline: 0.75rem;
  --space-2xs: 0.375rem;
  --space-xs: 0.5rem;
  --space-s: 0.75rem;
  --space-m: 1rem;
  --space-xl: 1.5rem;
  --space-2xl: 2.5rem;
  --space-4xl: 3.5rem;
  --space-6xl: 4.5rem;
  --space-8xl: 6rem;
}

.navSection {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-nav);
  padding: var(--space-xl) var(--space-4xl);
}

.header {
  background-color: #1f1f20;
  color: var(--color-white);
  overflow: hidden;
}

/* ── Bar ── */
.bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-s) var(--space-6xl);
  position: relative;
}

.logo {
  font-size: 2.25rem;
  font-weight: var(--weight-bold);
  color: var(--color-white);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.menuToggle {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  background: transparent;
  border: #1f1f20;
  color: var(--color-white);
  padding: var(--space-xs) var(--space-xl);
  font-size: var(--font-body-2xl);
  font-weight: var(--weight-large);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  border-radius: 0.25rem;
  transition: border-color 200ms ease;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}

.menuToggle:hover {
  border-color: rgba(255, 255, 255, 0.6);
}

.toggleLabel {
  font-size: var(--font-body);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.toggleIcon {
  font-size: var(--font-body);
  line-height: 1;
}

.ctaWrapper {
  display: flex;
  align-items: center;
  flex-direction: row;
  gap: var(--space-xs);
}

.ctaLabel {
  display: inline-flex;
  align-items: center;
  background-color: #1a6b3c;
  color: var(--color-white);
  padding: var(--space-xs) var(--space-xl);
  font-size: var(--font-body-sm);
  font-weight: var(--weight-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: 0.25rem;
  transition: background-color 200ms ease;
  overflow: hidden;
}

.ctaWrapper:hover .ctaLabel {
  background-color: #155630;
}

.ctaIcon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background-color: #1a6b3c;
  color: var(--color-white);
  width: 2.5rem;
  height: 2.15rem;
  border-radius: 0.25rem;
  overflow: hidden;
  transition: background-color 200ms ease;
}

.ctaWrapper:hover .ctaIcon {
  background-color: #155630;
}

.ctaIconInner {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ctaIconDefault {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: inherit;
  transition: transform 500ms cubic-bezier(0.65, 0, 0, 1);
  transform: translateY(0%);
}

.ctaWrapper:hover .ctaIconDefault {
  transform: translateY(-150%);
}

.ctaIconHover {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: inherit;
  position: absolute;
  transition: transform 500ms cubic-bezier(0.65, 0, 0, 1);
  transform: translateY(150%);
  transition-delay: 100ms;
}

.ctaWrapper:hover .ctaIconHover {
  transform: translateY(0%);
  transition-delay: 100ms;
}

/* ── Overlay ── */
.overlay {
  position: fixed;
  inset: 0;
  background-color: #1f1f20;
  opacity: 0;
  visibility: hidden;
  z-index: calc(var(--z-nav) - 1);
  transition: opacity 0.5s ease, visibility 0.5s ease;
}

.overlayVisible {
  opacity: 0.6;
  visibility: visible;
}

/* ── Panel ── */
.panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.6s cubic-bezier(0.625, 0.05, 0, 1);
}

.panelOpen {
  grid-template-rows: 1fr;
}

.panelClip {
  overflow: hidden;
  min-height: 0;
}

.panelInner {
  display: grid;
  grid-template-columns: 1fr 1fr 2fr;
  gap: var(--space-6xl);
  padding: var(--space-6xl) var(--space-8xl);
  border-top: 6px solid rgba(0, 0, 0, 0.25);
  background-color: #1f1f20;
}

/* ── Nav column ── */
.navList {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.navItem {
  overflow: hidden;
}

.navLink {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding-block: var(--space-xs);
  text-decoration: none;
}

/* Square — hidden by default, visible on active or hover */
.activeSquare {
  display: inline-block;
  width: 1.25rem;
  height: 1.25rem;
  background-color: #1a6b3c;
  flex-shrink: 0;
  opacity: 0;
  transform: scale(0.5);
  transition: opacity 300ms ease, transform 300ms ease;
}

.navLink:hover .activeSquare,
.navLinkActive .activeSquare {
  opacity: 1;
  transform: scale(1);
}

/* Label inner wrapper — clips the two text layers */
.navLabelInner {
  position: relative;
  overflow: hidden;
  display: block;
}

/* Default text — white, sits at translateY(0), slides up on hover */
.navLabelDefault {
  display: block;
  font-size: clamp(2rem, 4vw, 3.5rem);
  font-weight: var(--weight-medium);
  line-height: 1.1;
  color: var(--color-white);
  transition: transform 500ms cubic-bezier(0.65, 0, 0, 1);
  transform: translateY(0%);
}

.navLink:hover .navLabelDefault,
.navLinkActive .navLabelDefault {
  transform: translateY(-100%);
}

/* Hover text — green, starts below, slides up into view */
.navLabelHover {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  font-size: clamp(2rem, 4vw, 3.5rem);
  font-weight: var(--weight-medium);
  line-height: 1.1;
  color: #1a6b3c;
  transition: transform 500ms cubic-bezier(0.65, 0, 0, 1);
  transform: translateY(100%);
}

.navLink:hover .navLabelHover,
.navLinkActive .navLabelHover {
  transform: translateY(0%);
}

/* ── Contact column ── */
.contactCol {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
  padding-top: var(--space-xs);
}

.contactTagline {
  font-size: var(--font-body);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-gray);
}

.contactEmail {
  font-size: var(--font-body);
  color: var(--color-white);
  text-decoration: none;
  transition: color 200ms ease;
}

.contactSocials {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding-top: var(--space-m);
}

.contactSocialItem {
  font-size: var(--font-body-sm);
  color: var(--color-gray);
}

.notices {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
  margin-top: auto;
}

.noticesTagline {
  font-size: var(--font-tagline);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-gray);
}

.noticeRow {
  display: flex;
  align-items: flex-start;
  gap: var(--space-s);
}

.noticeSquare {
  display: inline-block;
  width: 0.625rem;
  height: 0.625rem;
  background-color: #1a6b3c;
  flex-shrink: 0;
  margin-top: 0.25rem;
}

.noticeText {
  font-size: var(--font-body-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-white);
}

/* ── Image column ── */
.imageCol {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-m);
  align-items: start;
}

.imageCard {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.imageWrap {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background-color: rgba(255, 255, 255, 0.08);
}

.cardImg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.imageCaption {
  font-size: var(--font-tagline);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-gray);
}

/* ── Tablet ── */
@media (max-width: 1023px) {
  .navSection {
    padding: 0;
  }

  .bar {
    padding: var(--space-s) var(--space-2xl);
  }

  .menuToggle {
    position: static;
    transform: none;
  }

  .ctaWrapper {
    display: none;
  }

  .panelInner {
    grid-template-columns: 1fr;
    padding: var(--space-4xl) var(--space-2xl);
    gap: var(--space-4xl);
  }

  .imageCol {
    display: none;
  }
}

/* ── Mobile ── */
@media (max-width: 767px) {
  .bar {
    padding: var(--space-s) var(--space-m);
  }

  .panelInner {
    padding: var(--space-4xl) var(--space-m);
  }
}

May 4, 2026

NAVIGATION

Nav Slide Panel

A minimal header that slides in from the top on load. Desktop shows logo, nav links, language switcher, and underlined contact CTA. Mobile collapses to logo + menu toggle with a full-screen white panel featuring large stagger-animated nav links, office locations, language selector, and copyright.

Apr 27, 2026

NAVIGATION

Nav Agency Grid

A fixed agency-style navigation bar using a 4-column CSS grid with staggered GSAP entrance animations, current-page indicator with animated dash, scroll-direction hide/show, desktop hover dropdown, and a full-screen mobile menu with large typography.

Apr 24, 2026

NAVIGATION

Nav Glassmorphic

A floating glassmorphic navigation bar with frosted semi-transparent background, centered nav links, locale selector, and outlined CTA button. Collapses to logo + hamburger on mobile with an expanding panel featuring nav links, social links, and email.