Detent web components
By Shane Logsdon

Navigation

Morphing Mega Menu

Implemented

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

custom items (one panel entry with an aside, one plain link)

Hover or click a trigger to morph the single panel; ArrowDown moves focus into the panel links, Escape closes it and returns focus to the trigger. Inactive panel contents are inert. The row records no events.

Recorded states

  • pressed recorded state; no setter
  • closed recorded state; no setter
  • expanded recorded state; no setter
  • reduced motion recorded state; no setter
  • active recorded 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

  • d
  • desc
  • href
  • title

Properties

  • icon
  • items

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.tsx sha256 5962cde5fc29…

Native files

The native module and its artifacts:

  • src/components/morphing-mega-menu/morphing-mega-menu.ts
  • src/components/morphing-mega-menu/styles.css
  • src/components/morphing-mega-menu/define.ts
  • docs/components/morphing-mega-menu.md
  • site/examples/morphing-mega-menu.ts
  • tests/browser/morphing-mega-menu.spec.ts
  • evidence/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

Attributes
AttributePropertyTypeDefaultNotes
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

Properties
PropertyTypeDefaultNotes
icon——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
itemsMegaMenuItem[]DEFAULT_MEGA_MENU_ITEMSProperty-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).
Previous: Icon Swap Next: Shape Loader

Docs

Components

Project