Detent web components
By Shane Logsdon

Navigation

Branching Submenu

Implemented

Animated tree navigation with keyboard support

detent-branching-submenu

Port status
Catalog status
implemented
Origin
upstream
Phase
3
Wave
phase-3-overlays
Group
Navigation

6.9 KB gzip for the class module · inlines the shared spring

Variants

custom label and items

ArrowDown/ArrowUp move focus between parents, ArrowRight and Enter open a branch, Escape closes it and returns focus to the active parent. The row records no events, so nothing is emitted by user actions or property writes.

Recorded states

  • focus-visible recorded state; no setter
  • reduced motion recorded state; no setter

Usage

Import

import '/components/branching-submenu/define.js';

// or register it yourself
import { DetentBranchingSubmenu } from '/components/branching-submenu/branching-submenu.js';

Attributes

  • label

Properties

  • items

Slots

No slots are recorded for this component.

Events

No events are recorded for this component.

Example

Runnable example: site/examples/branching-submenu.ts. The snippet below is generated from the catalogued attributes.

<detent-branching-submenu
  label="Branching navigation"
></detent-branching-submenu>

Property-only values

const element = document.querySelector('detent-branching-submenu');
if (element !== null) {
  // items: BranchingSubmenuItem[]
  element.items = [];
}

Source

The pinned upstream source for this row:

  • src/components/ui/BranchingSubmenu.tsx sha256 95144d7b4d80…

Native files

The native module and its artifacts:

  • src/components/branching-submenu/branching-submenu.ts
  • src/components/branching-submenu/styles.css
  • src/components/branching-submenu/define.ts
  • docs/components/branching-submenu.md
  • site/examples/branching-submenu.ts
  • tests/browser/branching-submenu.spec.ts
  • evidence/components/branching-submenu.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
labellabelstring'Branching navigation'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
itemsBranchingSubmenuItem[]demo 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

  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • items is plain data, rendered as text. Upstream accepted a BranchingSubmenuItem[] too, but rendered through React; here labels and descriptions go through textContent, so a payload like <script>window.x = 1</script> stays literal text (R-1.1). Invalid entries are dropped with one warning each instead of throwing.
  • Upstream motion is carried over: the selection pill glides between parents on springMorph (its layoutId); the branch content follows AnimatePresence mode="wait" on springGentle, the old branch springing out (opacity 0, y -6, scale 0.98, inert, focus moved to the active parent) before the new one springs in (y 6); and the children enter on upstream's tween (0.22 s, cubic-bezier(0.16, 1, 0.3, 1), index * 35 ms delay) through WAAPI, so the stagger no longer lingers as a transition-delay on hover. First paint does not animate (initial={false}). Not carried over: the containers' layout size animation, and the content exit when Escape closes the panel (it hides at once). Motion fidelity is a manual gate (PC-10.8).
  • Reduced motion removes all movement. Upstream swapped the animated pill for a static one and kept an opacity cross-fade; here the pill, the content swap and the child entrance do not run under prefers-reduced-motion: reduce, so the final state is immediate and fully usable (PC-10.5).
  • lucide-react's ChevronRight and Layers become one local, decorative inline chevron SVG (aria-hidden, PC-11.5). The Layers glyph next to the panel title is dropped: the panel is already labelled by panel-title.
  • Palette. The upstream dark-only hex values are replaced by theme tokens so the component works in light and dark (PC-9.5).
  • className is replaced by the documented parts and custom properties; no Tailwind and no React state/hooks are involved.
  • Escape also returns focus. Upstream only set open = false; the native port moves focus back to the active parent so the keyboard path has a defined target (PC-12.4).
  • No role="navigation" div. The region is a native <nav> element instead of a div[role="navigation"] (PC-12.1).

What it does

A labelled navigation region with a master list of parents and a branch panel that swaps its children when the active parent changes. Keyboard-explorable (Arrow keys, Enter, Escape), with a staggered child entrance and a responsive single-column stack.

Upstream behaviour
  • Parent and child panels communicate hierarchy through position and stagger
  • Arrow keys, Enter, and Escape support menu exploration
  • Responsive layout stacks cleanly on narrow screens
Differences from upstream
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • items is plain data, rendered as text. Upstream accepted a BranchingSubmenuItem[] too, but rendered through React; here labels and descriptions go through textContent, so a payload like <script>window.x = 1</script> stays literal text (R-1.1). Invalid entries are dropped with one warning each instead of throwing.
  • Upstream motion is carried over: the selection pill glides between parents on springMorph (its layoutId); the branch content follows AnimatePresence mode="wait" on springGentle, the old branch springing out (opacity 0, y -6, scale 0.98, inert, focus moved to the active parent) before the new one springs in (y 6); and the children enter on upstream's tween (0.22 s, cubic-bezier(0.16, 1, 0.3, 1), index * 35 ms delay) through WAAPI, so the stagger no longer lingers as a transition-delay on hover. First paint does not animate (initial={false}). Not carried over: the containers' layout size animation, and the content exit when Escape closes the panel (it hides at once). Motion fidelity is a manual gate (PC-10.8).
  • Reduced motion removes all movement. Upstream swapped the animated pill for a static one and kept an opacity cross-fade; here the pill, the content swap and the child entrance do not run under prefers-reduced-motion: reduce, so the final state is immediate and fully usable (PC-10.5).
  • lucide-react's ChevronRight and Layers become one local, decorative inline chevron SVG (aria-hidden, PC-11.5). The Layers glyph next to the panel title is dropped: the panel is already labelled by panel-title.
  • Palette. The upstream dark-only hex values are replaced by theme tokens so the component works in light and dark (PC-9.5).
  • className is replaced by the documented parts and custom properties; no Tailwind and no React state/hooks are involved.
  • Escape also returns focus. Upstream only set open = false; the native port moves focus back to the active parent so the keyboard path has a defined target (PC-12.4).
  • No role="navigation" div. The region is a native <nav> element instead of a div[role="navigation"] (PC-12.1).
Previous: Book Call Button Next: Breadcrumb

Docs

Components

Project