Unlimited Nested Menu
moumenlab/unlimited-nested-menuFreeComponent
A dropdown where each branch morphs into a stacked sub-panel, no depth limit, fully keyboard driven.
Live preview
live · rendered by the registry
Rendered by moumenlab on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://lab.moumen.dev/r/unlimited-nested-menu.jsonSource
1"use client";23import {4 useCallback,5 useEffect,6 useId,7 useLayoutEffect,8 useRef,9 useState,10 type CSSProperties,11 type KeyboardEvent,12 type ReactNode,13} from "react";14import {15 AnimatePresence,16 MotionConfig,17 animate,18 motion,19 useIsPresent,20 useReducedMotion,21} from "motion/react";2223// Unlimited nested menu - iOS-style stacked drill-down.24//25// The main dropdown never changes. Click a branch item and its children open as26// a NEW panel anchored right under that item, laid OVER the menu below it - the27// clicked item's name becomes the new panel's title (its header sits exactly28// where the item was, so the row appears to turn into the title). Do it again29// and the grandchildren stack the same way. There is no depth limit; parents30// stay visible and dimmed behind, and clicking one pops back to it.31//32// Motion (motion/react):33// - Opening the MENU: the root panel reveals from the trigger, scale + fade.34// - Drilling a branch: a shared-element morph - the child panel mounts clipped35// to just its header, which sits EXACTLY where the clicked row is and keeps36// the row's own icon, then opens - the clip expands downward while the37// title's weight crossfades plain -> bold (a measured, imperative animate()38// sequence, since the morph targets depend on the clicked row's geometry).39// - Any close (pop a level, or close the whole menu): AnimatePresence fades40// the leaving panel (or the whole stack) in place - shorter than the enter,41// no drift - with none of the exit-snapshot bookkeeping.42//43// Positioning is JS-measured against the popup origin, with a viewport-edge44// correction (--nm-shift-x) so a full-width panel near the screen edge slides to45// stay on-screen. Requires the lab-theme tokens. Fully Tailwind, no CSS files.4647const EASE = [0.22, 1, 0.36, 1] as const;48const POP_S = 0.22; // per-panel enter49const EXIT_S = 0.15; // exits are quicker + quieter than the enter5051// Panel shadows: the front panel owns the stack's elevation; panels behind it52// collapse to a crisp hairline so shadows don't compound with depth.53const SHADOW_ELEV =54 "shadow-[0_0_0_1px_rgba(0,0,0,0.07),0_14px_34px_-10px_rgba(0,0,0,0.26),0_5px_14px_-6px_rgba(0,0,0,0.12)]";55const SHADOW_BEHIND = "shadow-[0_0_0_1px_rgba(0,0,0,0.05),0_1px_2px_-1px_rgba(0,0,0,0.08)]";5657export interface NestedMenuItem {58 id?: string;59 label: string;60 icon?: ReactNode;61 hint?: string;62 danger?: boolean;63 disabled?: boolean;64 items?: NestedMenuItem[];65// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from moumenlab
All 14 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Caret-Anchored Mention Popovercaret-mention-popover | moumenlab | Components | Free | 1 dep | |
| Command Palette with Argument Chipscommand-palette | moumenlab | Components | Free | 1 dep | |
| Drag-to-Reorder Listdrag-to-reorder-list | moumenlab | Components | Free | 1 dep | |
| File Upload Staging Areafile-upload-staging | moumenlab | Components | Free | 1 dep | |
| Hover-Expand Icon Striphover-expand-icon-strip | moumenlab | Components | Free | 1 dep | |
| Inertial Wheel Listinertial-wheel-list | moumenlab | Components | Free | 1 dep | |
| Morphing Checkout Flowmorphing-checkout | moumenlab | Components | Free | 1 dep | |
| OTP Segmented Inputotp-segmented-input | moumenlab | Components | Free | 1 dep |
