Live Preview

FLIGHT

Demo preview: use Menu to test interaction.

Get Started

Before placing this component on another site, confirm:

  • React app with client component support ('use client').
  • Set your own nav labels and links based on your project structure.
  • Styling setup: Tailwind CSS (as written) or mapped custom CSS.
  • Your own logo image URL (replace the current Cloudinary src).
  • Pick the Next.js or Vite code version below, then customize links/components as needed.

Available Props

Use these props to customize the component from your page code:

Prop Type Default What it controls
navItems { label, href }[] Studio / Journal / Archive Drawer menu labels and links
leftLabel string "Flight Navigation" Text shown on the left side of the nav bar
homeHref string "/" Destination when clicking the center logo
logoSrc string Cloudinary Flight image URL Logo image source
logoAlt string "Flight" Accessible image alt text
scrollBehavior { hideOnScrollDown, cruiseMode, solidAfterScroll } All false Controls hide/show, nav height shrink, and transparent/solid surface behavior on scroll
menuAppear { mode, direction } { mode: "none", direction: "down" } Controls drawer entrance style: no effect, fade from top, or directional slide

Flight Component

Next.js + React

'use client';

import { useEffect, useState } from 'react';
import Link from 'next/link';

export function Flight({
  navItems = [
    { label: 'Studio', href: '#' },
    { label: 'Journal', href: '#' },
    { label: 'Archive', href: '#' },
  ],
  leftLabel = 'Flight Navigation',
  homeHref = '/',
  logoSrc = 'https://res.cloudinary.com/dtkhwq0je/image/upload/v1769378182/Asset_1_dhwm3l.png',
  logoAlt = 'Flight',
  scrollBehavior = {},
  menuAppear = {},
}) {
  const [isOpen, setIsOpen] = useState(false);
  const [isHidden, setIsHidden] = useState(false);
  const [isCruise, setIsCruise] = useState(false);
  const [isScrolled, setIsScrolled] = useState(false);

  const {
    hideOnScrollDown = false,
    cruiseMode = false,
    solidAfterScroll = false,
  } = scrollBehavior;
  const { mode = 'none', direction = 'down' } = menuAppear;

  useEffect(() => {
    let lastScrollY = window.scrollY;

    const onScroll = () => {
      const currentScrollY = window.scrollY;

      if (hideOnScrollDown) {
        if (currentScrollY > lastScrollY && currentScrollY > 16) {
          setIsHidden(true);
        } else {
          setIsHidden(false);
        }
      } else {
        setIsHidden(false);
      }

      if (cruiseMode) {
        setIsCruise(currentScrollY > 8);
      } else {
        setIsCruise(false);
      }

      if (solidAfterScroll) {
        setIsScrolled(currentScrollY > 8);
      } else {
        setIsScrolled(false);
      }

      lastScrollY = currentScrollY;
    };

    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });

    return () => window.removeEventListener('scroll', onScroll);
  }, [hideOnScrollDown, cruiseMode, solidAfterScroll]);

  const navVisibilityClass = hideOnScrollDown && isHidden ? '-translate-y-full' : 'translate-y-0';
  const navHeightClass = cruiseMode && isCruise ? 'h-12' : 'h-16';
  const navSurfaceClass = solidAfterScroll
    ? isScrolled
      ? 'bg-black/90'
      : 'bg-transparent'
    : 'bg-black/35 backdrop-blur-2xl backdrop-brightness-65';
  const directionMap = {
    up: '-translate-y-8',
    down: 'translate-y-8',
    left: '-translate-x-8',
    right: 'translate-x-8',
  };
  const slideHiddenClass = directionMap[direction] || directionMap.down;
  const drawerOpenClass = isOpen ? 'opacity-100 pointer-events-auto translate-x-0 translate-y-0' : '';
  const drawerClosedClass =
    mode === 'fadeTop'
      ? 'opacity-0 pointer-events-none -translate-y-6'
      : mode === 'slide'
        ? `opacity-0 pointer-events-none ${slideHiddenClass}`
        : 'opacity-0 pointer-events-none';
  const drawerMotionClass =
    mode === 'none'
      ? ''
      : mode === 'fadeTop'
        ? 'transition-all duration-300 ease-out'
        : 'transition-all duration-300 ease-in-out';

  return (
    <>
      <nav className={`fixed top-0 left-0 right-0 z-40 transition-all duration-300 ${navVisibilityClass} ${navSurfaceClass} shadow-[0_12px_40px_rgba(0,0,0,0.18)]`}>
        <div id="nav-shell" className="w-full px-2 sm:px-4 lg:px-6">
          <div id="nav-row" className={`flex justify-between items-center ${navHeightClass}`}>
            {/* Left: Globe + Label */}
            <div id="nav-left" className="flex-1 flex items-center gap-2 text-white">
              <svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                <circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" />
                <path d="M3 12h18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
                <path d="M12 3c2.5 2.7 4 5.8 4 9s-1.5 6.3-4 9c-2.5-2.7-4-5.8-4-9s1.5-6.3 4-9Z" stroke="currentColor" strokeWidth="2" />
              </svg>
              <span className="font-[var(--font-dm-sans)] text-sm font-extralight tracking-wide text-white">
                {leftLabel}
              </span>
            </div>

            {/* Center: Logo/Brand */}
            <div id="nav-center" className="flex-1 flex justify-center">
              <Link href={homeHref} aria-label="Go to home">
                <img
                  src={logoSrc}
                  alt={logoAlt}
                  className="h-8 w-auto"
                />
              </Link>
            </div>

            {/* Menu Toggle */}
            <div id="nav-right" className="flex-1 flex items-center justify-end gap-3">
              <button
                onClick={() => setIsOpen(!isOpen)}
                className="group inline-flex items-center justify-center px-4 py-2 rounded-md text-white/70 hover:text-white focus:outline-none transition-colors duration-300"
                aria-label="Toggle drawer menu"
              >
                <span className="text-lg font-semibold text-white/70 group-hover:text-white underline-offset-4 group-hover:underline">
                  {isOpen ? "Close" : "Menu"}
                </span>
              </button>
            </div>
          </div>
        </div>
      </nav>

      {/* Full-Screen Drawer Menu */}
      <div
        id="drawer"
        className={`fixed top-16 left-0 right-0 bottom-0 z-30 bg-[#FAF9F5] ${drawerMotionClass} ${
          isOpen ? drawerOpenClass : drawerClosedClass
        }`}
      >
        <div id="drawer-content" className="flex flex-col items-center justify-center h-full space-y-8 px-6">
          {navItems.map((item) => (
            <a
              key={item.label}
              href={item.href}
              className="font-[var(--font-dm-sans)] text-4xl font-light text-[#1b1a16] hover:text-[#8f7220] transition-colors"
              onClick={() => setIsOpen(false)}
            >
              {item.label}
            </a>
          ))}
        </div>
      </div>
    </>
  );
}

Usage

Next.js + React

import { Flight } from "@/components/flight";

const navItems = [
  { label: "Studio", href: "/studio" },
  { label: "Journal", href: "/journal" },
  { label: "Archive", href: "/archive" },
];

const scrollBehavior = {
  hideOnScrollDown: true,
  cruiseMode: true,
  solidAfterScroll: true,
};

const menuAppear = {
  mode: "slide",
  direction: "down",
};

export default function HomePage() {
  return (
    <>
      <Flight
        navItems={navItems}
        leftLabel="Webstaurants"
        homeHref="/"
        logoSrc="https://res.cloudinary.com/dtkhwq0je/image/upload/v1769378182/Asset_1_dhwm3l.png"
        logoAlt="Webstaurants logo"
        scrollBehavior={scrollBehavior}
        menuAppear={menuAppear}
      />
      <main className="pt-16">{/* page content */}</main>
    </>
  );
}

Flight Styles

CSS

/*
  Current Flight component styling is mostly inline Tailwind utility classes.
  This is a separated CSS version you can use as a base if you want to move styles out of JSX.
*/

#nav-shell {
  width: 100%;
  padding-left: 0.5rem;
  padding-right: 0.5rem;
}

@media (min-width: 640px) {
  #nav-shell {
    padding-left: 1rem;
    padding-right: 1rem;
  }
}

@media (min-width: 1024px) {
  #nav-shell {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
  }
}

#nav-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 4rem;
}

#nav-left,
#nav-center,
#nav-right {
  flex: 1;
}

#nav-left {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: #fff;
}

#nav-center {
  display: flex;
  justify-content: center;
}

#nav-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
}

#drawer {
  position: fixed;
  top: 4rem;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 30;
  transform: translateY(-100%);
  opacity: 0;
  pointer-events: none;
  transition: all 0.3s ease-in-out;
}

#drawer.is-open {
  background-color: #faf9f5;
  transform: translateY(0);
  opacity: 1;
  pointer-events: auto;
}

#drawer-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  gap: 2rem;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

Flight

A sleek and compact navigation component

Split accordion

A accordion and image section