ph/components

Navbar

Logo, links and a mobile menu.

Code

Copy into src/components/ph/ .

navbar.css
.ph-navbar__burger-line {
  display: block;
  width: 28px;
  height: 4px;
  background-color: currentColor;
  transition:
    transform 300ms ease,
    opacity 300ms ease;
}

.ph-navbar__burger[aria-expanded="true"] .ph-navbar__burger-line:nth-child(1) {
  transform: translateY(10px) rotate(45deg);
}

.ph-navbar__burger[aria-expanded="true"] .ph-navbar__burger-line:nth-child(2) {
  opacity: 0;
}

.ph-navbar__burger[aria-expanded="true"] .ph-navbar__burger-line:nth-child(3) {
  transform: translateY(-10px) rotate(-45deg);
}

.ph-navbar__menu {
  visibility: hidden;
  transform: translateY(-100%);
  transition:
    transform 700ms cubic-bezier(0.12, 0.85, 0.25, 1),
    visibility 700ms;
}

.ph-navbar__menu[data-open] {
  visibility: visible;
  transform: translateY(0);
}

.ph-navbar__menu-link {
  color: var(--ph-cream);
  font-family: var(--ph-font-mono);
  font-size: 3rem;
  font-weight: 600;
  letter-spacing: 0.025em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color 200ms;
}

.ph-navbar__menu-link:hover,
.ph-navbar__menu-link:focus-visible {
  color: var(--ph-yellow);
}

.ph-navbar[data-menu-open] .ph-navbar__bar {
  color: var(--ph-cream);
}

@media (prefers-reduced-motion: reduce) {
  .ph-navbar__burger-line,
  .ph-navbar__menu {
    transition: none;
  }
}

.ph-navbar[data-tone="dark"] .ph-navbar__bar {
  color: var(--ph-cream);
}
Navbar.astro
---
import Glider from "./Glider.astro";
import NavLink from "./NavLink.astro";
import "./navbar.css";

type NavItem = {
  label: string;
  href: string;
};

type Props = {
  links: NavItem[];
  homeHref?: string;
  tone?: "light" | "dark";
  current?: string;
  class?: string;
};

const { links, homeHref = "/", tone, current, class: className } = Astro.props;
const external = (href: string) => /^https?:\/\//.test(href);
---

<header data-ph-navbar data-tone={tone} class:list={["ph-navbar relative z-30", className]}>
  <div class="ph-navbar__bar relative z-30 flex items-center justify-between px-6 py-5 md:px-8 md:py-6">
    <a href={homeHref} aria-label="Purdue Hackers home" class="inline-flex text-current transition-opacity hover:opacity-70">
      <Glider class="size-8" />
    </a>

    <nav class="hidden gap-8 md:flex">
      {
        links.map((l) => (
          <NavLink href={l.href} aria-current={current === l.href ? "page" : undefined}>
            {l.label}
          </NavLink>
        ))
      }
    </nav>

    <button
      type="button"
      data-ph-navbar-toggle
      aria-expanded="false"
      aria-label="Open menu"
      class="ph-navbar__burger ml-auto flex size-10 cursor-pointer flex-col items-center justify-center gap-1.5 md:hidden"
    >
      <span class="ph-navbar__burger-line" aria-hidden="true"></span>
      <span class="ph-navbar__burger-line" aria-hidden="true"></span>
      <span class="ph-navbar__burger-line" aria-hidden="true"></span>
    </button>
  </div>

  <div data-ph-navbar-menu class="ph-navbar__menu fixed inset-0 z-20 bg-ph-black md:hidden" aria-hidden="true">
    <nav class="flex min-h-full flex-col justify-center gap-6 px-8 py-24">
      {
        links.map((l) => (
          <a
            href={l.href}
            target={external(l.href) ? "_blank" : undefined}
            rel={external(l.href) ? "noopener noreferrer" : undefined}
            class="ph-navbar__menu-link"
          >
            {l.label}
          </a>
        ))
      }
    </nav>
  </div>
</header>

<script>
  for (const bar of document.querySelectorAll<HTMLElement>("[data-ph-navbar]")) {
    const toggle = bar.querySelector<HTMLButtonElement>("[data-ph-navbar-toggle]")!;
    const menu = bar.querySelector<HTMLElement>("[data-ph-navbar-menu]")!;

    const set = (open: boolean) => {
      toggle.setAttribute("aria-expanded", String(open));
      toggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
      menu.toggleAttribute("data-open", open);
      menu.setAttribute("aria-hidden", String(!open));
      menu.inert = !open;
      bar.toggleAttribute("data-menu-open", open);
      document.documentElement.style.overflow = open ? "hidden" : "";
    };

    set(false);
    toggle.addEventListener("click", () => set(toggle.getAttribute("aria-expanded") !== "true"));
    menu.addEventListener("click", (e) => {
      if ((e.target as HTMLElement).closest("a")) set(false);
    });
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape") set(false);
    });
  }
</script>

Also needs