Skip to content
WANCRAFT

D.78Navigation & Menus

Morphing Dropdown

A top-nav dropdown where one floating panel morphs its size and position between menus while content slides.

NewReactMotionTailwindadvanced

Overview

Morphing Dropdown gives a row of navigation triggers a single shared floating panel. Moving between menus does not close and reopen anything: the panel springs to the new content's measured width, height and position, the content slides in from the direction you travelled, and an arrow pointer glides along to sit under the active trigger. Mice get hover intent with an open delay and a close grace period; touch uses tap to toggle; keyboards open with Enter, Space or ArrowDown, move between triggers with the arrow keys and close with Escape, which returns focus to the trigger. Each trigger is a disclosure button with aria-expanded and aria-controls, and Tab moves from an open trigger straight into its content.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/morphing-dropdown.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/morphing-dropdown.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/morphing-dropdown.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { MorphingDropdown, type MorphingDropdownItem } from "@/components/ui/morphing-dropdown";

export function MorphingDropdownExample() {
  const items: MorphingDropdownItem[] = [
    { id: "product", label: "Product", content: <div className="w-[420px] p-5">Product links</div> },
    { id: "resources", label: "Resources", content: <div className="w-[320px] p-5">Docs and guides</div> },
    { id: "company", label: "Company", content: <div className="w-[260px] p-5">About, careers</div> },
  ];

  return (
    <header className="flex justify-center p-6">
      <MorphingDropdown items={items} />
    </header>
  );
}

03Props

PropTypeDefaultDescription
itemsMorphingDropdownItem[]—{ id: string; label: string; content: ReactNode }. Content's intrinsic size drives the panel.
openOnHoverbooleantrueOpen on mouse hover (touch and keyboard always use click / Enter).
openDelaynumber120Hover-intent delay before the first menu opens, in ms.
closeDelaynumber240Grace period after the pointer leaves, in ms.
slideDistancenumber48How far content slides when switching menus, in px.
showArrowbooleantrueShow the arrow pointer above the panel.
spring{ stiffness: number; damping: number; mass: number }{ stiffness: 420, damping: 38, mass: 0.9 }Spring for panel size, position and arrow.
accentstring"#FF7448"Focus ring colour.
labelstring"Main"Accessible name of the nav landmark.
defaultValuestring | nullnullId of the menu open on first render.
onValueChange(id: string | null) => void—Called when the open menu changes.
...propsHTMLAttributes<HTMLElement>—Native attributes are forwarded to the <nav>.

04Features

  • 01One shared panel that springs to each menu's measured width, height and position
  • 02Directional content slide based on which way you moved along the triggers
  • 03Arrow pointer that glides to the active trigger; the panel stays inside the viewport
  • 04Hover intent with open delay and close grace period, plus a pointer bridge over the gap
  • 05Touch taps toggle; a click right after hover-open does not slam the menu shut
  • 06Keyboard: Enter / Space / ArrowDown open, arrows and Home / End move, Escape closes with focus return
motion · springmotion · layoutinput · hoverinput · clickinput · keyboardinput · focus

05Accessibility

  • Rendered as a labelled <nav>; each trigger is a disclosure <button> with aria-expanded and aria-controls pointing at its open content.
  • ArrowDown opens a menu and focuses its first link; Tab from an open trigger moves into the panel, and past the last link continues with the next trigger.
  • Escape closes from anywhere in the navigation and returns focus to the trigger; focus or a click leaving the navigation closes it.
  • The arrow pointer is aria-hidden; content is regular DOM, so links and headings keep their semantics.
  • With prefers-reduced-motion the panel resizes and moves instantly and content cross-fades without sliding.

06Limitations

  • Designed for horizontal desktop navigation; on very narrow screens pair it with a mobile menu such as Overlay Menu.