Navigation
Morphing Mega Menu
Stripe-style morphing navigation panel with spring physics and cursor tracking
detent-morphing-mega-menu
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 3
- Wave
- phase-3-overlays
- Group
- Navigation
7.2 KB gzip for the class module · inlines the shared spring
Variants
Recorded states
pressedrecorded state; no setterclosedrecorded state; no setterexpandedrecorded state; no setterreduced motionrecorded state; no setteractiverecorded state; no setter
Usage
Import
import '/components/morphing-mega-menu/define.js';
// or register it yourself
import { DetentMorphingMegaMenu } from '/components/morphing-mega-menu/morphing-mega-menu.js';
Attributes
ddeschreftitle
Properties
iconitems
Slots
No slots are recorded for this component.
Events
No events are recorded for this component.
Example
Runnable example: site/examples/morphing-mega-menu.ts. The snippet below is generated from the catalogued attributes.
<detent-morphing-mega-menu
d
desc
href
title
></detent-morphing-mega-menu>
Property-only values
const element = document.querySelector('detent-morphing-mega-menu');
if (element !== null) {
// items: MegaMenuItem[]
element.items = [];
}
Source
The pinned upstream source for this row:
src/components/ui/MorphingMegaMenu.tsxsha256 5962cde5fc29…
Native files
The native module and its artifacts:
src/components/morphing-mega-menu/morphing-mega-menu.tssrc/components/morphing-mega-menu/styles.csssrc/components/morphing-mega-menu/define.tsdocs/components/morphing-mega-menu.mdsite/examples/morphing-mega-menu.tstests/browser/morphing-mega-menu.spec.tsevidence/components/morphing-mega-menu.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
d | — | — | — | Recorded in the catalog as an attribute. |
desc | — | — | — | Recorded in the catalog as an attribute. |
href | — | — | — | Recorded in the catalog as an attribute. |
title | — | — | — | Recorded in the catalog as an attribute. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
icon | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
items | MegaMenuItem[] | DEFAULT_MEGA_MENU_ITEMS | Property-only: assign from script; not accepted as an attribute. |
Events
No custom events are recorded for this component.
Slots
No slots are recorded for this component.
CSS customisation
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- content: ReactNode becomes plain data. Upstream rendered arbitrary React nodes per panel; the native row records no slots, so panel content is links/aside plain data rendered as text inside real anchors (R-1.1/PC-11.1). A consumer that needs arbitrary markup needs a slot-bearing row.
- MenuLink is a type, not a component. It shipped as a React component with icon/title/desc/href; here it ships as that same plain-data shape and the element renders it. The row's attributes (d, desc, href, title) describe MenuLink/Icon params, not host attributes; the element itself consumes no attributes.
- Icons are local. lucide-free: the upstream ICONS path data is a local module constant and the glyphs are decorative (aria-hidden, PC-11.5).
- Absolute positioning, not the top layer. One surface that morphs between trigger positions cannot live in a popover/<dialog> while the bar stays in flow. Clipping limitation: an overflow: hidden/clip ancestor of the element clips the open panel; an open host raises itself above sibling components, but a sibling with a higher z-index still wins.
- Framer Motion becomes CSS plus one spring. The panel's width/height/transform morph is a CSS transition driven by ResizeObserver measurements; the arrow indicator uses the shared spring helper with the pinned upstream values (600/45/0.6) and snaps on the first open (arrowSpring.jump). The upstream point-measured geometry() clamp is reproduced, but the { duration: 0.4, bounce: 0.1 } morph spring is not reproduced numerically — motion fidelity is a manual gate (PC-10.8) and is recorded as unresolved.
- Reduced motion removes both. Upstream only shortened the position update; here the transitions do not exist and the arrow writes its target directly, so the final state is immediate and usable (PC-10.5).
- Click after hover keeps the panel open. Upstream's onClick toggled unconditionally, so a click following a hover-open closed what hover had just opened; the native port tracks how the panel was opened and only a second activation closes it.
- Palette. The upstream neutral hex/Tailwind pair is replaced by theme tokens so light and dark both work, and the panel is raised while open (PC-9.5).
What it does
A centred navigation bar whose one panel morphs its width, height and x-position between triggers, with an arrow indicator that tracks the pointer, ResizeObserver-measured content, ArrowDown traversal into the panel and Escape dismissal.
Upstream behaviour
- Single morphing surface that smoothly animates width, height, and x-position
- Spring-tracked arrow indicator following pointer movement along the navigation triggers
- ResizeObserver-driven dimension calculation for arbitrary nested menus
- Full keyboard accessibility including ArrowDown direct content traversal and Esc dismissal
- Graceful touch outside-press dismissal and reduced motion awareness
Differences from upstream
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- content: ReactNode becomes plain data. Upstream rendered arbitrary React nodes per panel; the native row records no slots, so panel content is links/aside plain data rendered as text inside real anchors (R-1.1/PC-11.1). A consumer that needs arbitrary markup needs a slot-bearing row.
- MenuLink is a type, not a component. It shipped as a React component with icon/title/desc/href; here it ships as that same plain-data shape and the element renders it. The row's attributes (d, desc, href, title) describe MenuLink/Icon params, not host attributes; the element itself consumes no attributes.
- Icons are local. lucide-free: the upstream ICONS path data is a local module constant and the glyphs are decorative (aria-hidden, PC-11.5).
- Absolute positioning, not the top layer. One surface that morphs between trigger positions cannot live in a popover/<dialog> while the bar stays in flow. Clipping limitation: an overflow: hidden/clip ancestor of the element clips the open panel; an open host raises itself above sibling components, but a sibling with a higher z-index still wins.
- Framer Motion becomes CSS plus one spring. The panel's width/height/transform morph is a CSS transition driven by ResizeObserver measurements; the arrow indicator uses the shared spring helper with the pinned upstream values (600/45/0.6) and snaps on the first open (arrowSpring.jump). The upstream point-measured geometry() clamp is reproduced, but the { duration: 0.4, bounce: 0.1 } morph spring is not reproduced numerically — motion fidelity is a manual gate (PC-10.8) and is recorded as unresolved.
- Reduced motion removes both. Upstream only shortened the position update; here the transitions do not exist and the arrow writes its target directly, so the final state is immediate and usable (PC-10.5).
- Click after hover keeps the panel open. Upstream's onClick toggled unconditionally, so a click following a hover-open closed what hover had just opened; the native port tracks how the panel was opened and only a second activation closes it.
- Palette. The upstream neutral hex/Tailwind pair is replaced by theme tokens so light and dark both work, and the panel is raised while open (PC-9.5).