ProductBrand
Join us
Foundations
  • Introduction
  • Guidelines
  • Colors
  • Typography
  • Breakpoints
  • Elevation
  • Patterns
Identity
  • Logos
  • Mr Rogo
  • Assets
Components
  • Button
  • Carousel
  • CTA Headline
  • Navigation Menu
  • Popover
  • Separator Dot
  • Sheet
  • Skeleton
  • Video Player

Navigation Menu

The marketing site's primary navigation, with sub-link panels.

  • Customers

Overview

A row of top-level entries where some open a panel of sub-links. It wraps Base UI's NavigationMenu, so hover intent, the keyboard model (arrow keys across the row, Escape to close, focus returning to the trigger) and the transition between panels come from a maintained primitive rather than being rebuilt per campaign.

Nothing here hardcodes Rogo's menu, since you compose the parts and pass your own entries, which is what makes it reusable for a one-off landing page on a subdomain.

tsx
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuPanel,
  NavigationMenuPanelLink,
  NavigationMenuTrigger,
} from "@rogo-technologies/brand/navigation-menu";

Anatomy

tsx
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Product</NavigationMenuTrigger>
      <NavigationMenuContent className="grid w-[420px] gap-1">
        <NavigationMenuPanelLink href="/product" title="Platform" description="…" />
      </NavigationMenuContent>
    </NavigationMenuItem>

    <NavigationMenuItem>
      <NavigationMenuLink href="/customers">Customers</NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenuList>

  <NavigationMenuPanel />
</NavigationMenu>

NavigationMenuPanel is required and goes last, as a sibling of the list rather than inside it. It is the portal, positioner, popup and viewport in one part: Base UI needs all four and their nesting order is load-bearing, so the wrapper composes them to remove the most common way to get this wrong. One Panel serves every item; the open item's Content is rendered into it.

Parts

PartWhat it is
NavigationMenuRoot. Owns open state and hover intent.
NavigationMenuListThe row. Renders a <ul>.
NavigationMenuItemOne entry in the row.
NavigationMenuTriggerAn entry that opens a panel.
NavigationMenuLinkAn entry that just navigates. Stays in the same arrow-key row as triggers.
NavigationMenuContentThe panel body for one item. Set its width and grid here.
NavigationMenuPanelLinkA destination inside a panel. sm takes title plus optional description; display is title-only.
NavigationMenuPanelFeatureA 150px-tall picture with title and supporting line stacked under it: media, title, description.
NavigationMenuPanelThe shared popup the open item renders into. Required, once, last.

The panel morphs between items

Moving from one trigger to another keeps the same popup rather than closing and reopening it. An anchored card resizes and slides to the new trigger; a full panel stays a fixed height and crossfades its contents. That is not automatic. It needs three things, which the wrapper already does:

  • the popup binds its size to --popup-width / --popup-height (anchored) or to a fixed height (full);
  • the viewport is relative + overflow-hidden so the outgoing and incoming panels can overlap;
  • Content stays in flow. Giving it position: absolute collapses the measurement to zero and the panel renders as a dot.

Content slides in from the side the pointer came from via data-activation-direction on an anchored card. A full panel sets --nav-menu-slide to 0, so the same classes become a 200ms linear fade. data-instant disables the transition when Base UI wants an immediate jump.

Animating size is layout work on every frame, so the anchored popup is deliberately not promoted to its own compositor layer, since a promoted layer would be re-rasterised on each of those frames.

Every transition list here names scale / translate / rotate / clip-path rather than transform. Tailwind v4 compiles those utilities to the standalone CSS properties, so transition-transform animates nothing and the panel snaps between states while only the opacity fades, which reads as lag, not as a broken class.

Two panel shapes

NavigationMenuPanel takes a variant:

variantWhat it is
anchored (default)A floating card under its own trigger. Morphs width and height between items.
fullSpans the anchor edge to edge, flush under it. Fixed height; contents crossfade.
  • Customers

A full panel needs an anchor as wide as the page, because the default anchor is the trigger and the panel would come out the width of one nav label:

tsx
const headerRef = useRef<HTMLElement>(null);

<header ref={headerRef}>
  <NavigationMenu>
    …
    <NavigationMenuPanel variant="full" anchor={headerRef} />
  </NavigationMenu>
</header>;

It carries a 30% black scrim over the page below the header (light and dark chrome alike), and pins itself with position: fixed so it tracks a sticky header instead of being left at the scroll offset it opened at. Give its Content w-full p-0 and set the page's own gutters inside, since the panel is the page's shell from its top edge down, not a card with padding.

Open, Enter, and Leave are separate for the dropdown and the scrim, matching the Framer overlay:

  • Dropdown Open / Leave — clip-path wipe inset(0 0 100% 0) ↔ inset(0), 400ms, cubic-bezier(0.22, 1, 0.36, 1) (Framer spring-duration 0.4s bounce 0.2). Leave reverses the same wipe; it is not the Appear tween.
  • Content Enter — Appear, 500ms, cubic-bezier(0.12, 0.23, 0.5, 1), y: 12, stagger 100ms (same as the header groups' 1.0 / 1.1 / 1.2 delays).
  • Scrim Open — opacity fade, 400ms, cubic-bezier(0.5, 0, 0.88, 0.77) (Framer backdrop enter; serialize duration 0s inherits overlay Open).
  • Scrim Leave — opacity fade, 500ms, cubic-bezier(0.12, 0.23, 0.5, 1) (Framer Appear / backdrop exit).

Enter for the panel's contents (staggered Appear) lives on the consumer, so moving between items does not replay it. Base UI measures the popup's computed height in a layout effect while data-starting-style is still on it, so any starting height would be measured as the panel's real one and lock it there. Clip-path is invisible to that measurement, costs no layout, and stays interruptible. Moving between items does not resize the box — contents crossfade at that fixed height (min(320px, 90vh)).

The scrim is a sibling of the popup, not a child, so the wipe does not clip it, and it sits behind the panel (z-0 / z-10) so the panel paints on top. There is no shadow on the panel: the header's bottom border is the only edge, and a drop shadow on the wipe would paint a moving line the clip is already drawing.

The divider above the panel belongs to the header, not to the panel. The panel sits flush against the header's bottom border, so drawing its own top edge would put two hairlines there. The website turns the header's border on for an open menu with has-[[data-popup-open]]:border-marketing-border, read off the open trigger rather than mirrored into React state.

Idle labels in the row sit at full opacity. Hovering one, focusing one, or opening a panel sets the others to 60%. Opacity is set on the items so enter and leave both interpolate.

Links come in two sizes

NavigationMenuPanelLink takes a size:

sizeWhat it is
sm (default)A row in a floating card: 14px title, supporting line, plate behind it on hover.
displayA single-line destination at type-marketing-display-md (40px). No plate, no description.

display is what a full panel holds. Hover (or keyboard focus) keeps that link at full ink and fades the other display links to 40% — the invert lives on the links themselves so leave interpolates. The feature card is a separate link and is left out of both the trigger and the fade, so hovering a destination does not dim the picture.

A full panel lays out as three equal columns: two stacks of up to three destinations, then the feature. The stacks share a top so a short second column does not sink; the third column stays reserved when there is nothing to preview, so the stacks do not shift.

The chevron is built in

NavigationMenuTrigger renders its own chevron through Base UI's Icon part, so it flips with the panel and you never pass one:

tsx
<NavigationMenuTrigger>Product</NavigationMenuTrigger>

Icons come from @rogo-technologies/brand/icons, the same central-icons set the product uses. Never hand-draw one.

Use a plain link when there is no panel

NavigationMenuLink rather than a bare <a>: Base UI keeps it in the roving-focus row, so arrow keys move across the entire nav instead of skipping past the entries that have no panel.

Responsive

The component is the desktop row. It has no built-in mobile treatment, because the panels are hover-driven and there is no room for them on a phone. Build the small-screen navigation from Sheet and swap on your breakpoint, which is what the website does.

PreviousCTA Headline
NextPopover
Made in NYC© 2026 Rogo Technologies Inc.

Design at Rogo

We’re redesigning an entire industry. Come design it with us.

See open roles

Design at Rogo

  • How we workThe mission of design at Rogo, and the open roles
  • About RogoThe company and the product