Detent web components
By Shane Logsdon

Navigation

Pill Navigation

Implemented

Minimal shared-pill navigation continuity with fluid submenus

detent-pill-navigation

Port status
Catalog status
implemented
Origin
upstream
Phase
2
Wave
phase-2-navigation
Group
Navigation

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

Variants

Recorded states

  • expanded recorded state; no setter
  • selected recorded state; no setter
  • reduced motion recorded state; no setter

Usage

Import

import '/components/pill-navigation/define.js';

// or register it yourself
import { DetentPillNavigation } from '/components/pill-navigation/pill-navigation.js';

Attributes

  • default-sub-value
  • default-value

Properties

  • items
  • onChange

Slots

No slots are recorded for this component.

Events

  • detent-change — { id: string; subId?: string }

Example

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

<detent-pill-navigation
  default-sub-value
  default-value
></detent-pill-navigation>

Property-only values

const element = document.querySelector('detent-pill-navigation');
if (element !== null) {
  // items: PillNavigationItem[]
  element.items = [];
  // onChange: (id: string, subId?: string) => void
  element.onChange = () => {};
}

Source

The pinned upstream source for this row:

  • src/components/ui/PillNavigation.tsx sha256 ee9aa1f8098f…

Native files

The native module and its artifacts:

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

API

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

Attributes

Attributes
AttributePropertyTypeDefaultNotes
default-sub-valuedefaultSubValuestringundefinedKebab-case attribute; camelCase property.
default-valuedefaultValuestringfirst item idKebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
itemsPillNavigationItem[]demo itemsProperty-only: assign from script; not accepted as an attribute.
onChange(id: string, subId?: string) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-changeonChange(id: string, subId?: string) => voidComposed, bubbles; fires only for a user action.

Slots

No slots are recorded for this component.

CSS customisation

  • React callback props become typed composed CustomEvents; programmatic state assignment emits none.
  • 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.
  • Controlled/uncontrolled React pair maps to initial-value attributes plus owned state and change events.
  • `items` defaults to `[]`; upstream shipped a hard-coded demo list as its default, which is demo data, not API - the preview and example carry it.
  • `default-value`/`default-sub-value` are initial-value attributes (they seed the first render, not re-applied), matching upstream's uncontrolled useState pair; there is no controlled value or programmatic selection setter.
  • Both tiers use tablist/tab with a named submenu and roving tabindex: upstream's role=menu/menuitem, invalid aria-checked and redundant wrapper tabindex are removed (PC-12.6).
  • ArrowDown/ArrowUp open a closed submenu instead of silently changing a hidden sub-selection.
  • `PillSubItem.badge` is declared upstream but never rendered by the source, so it stays in the type and renders nothing.
  • Both pills are single elements driven by the shared helper with the recorded springMorph (main) and springGentle (sub); AnimatePresence becomes a CSS entrance keyframe replayed when the active parent changes.
  • Upstream's dark-only hex palette becomes --detent-color-*, and the submenu outline is raised to 3:1 because its raised surface matches the page in light mode.

What it does

A segmented pill bar with one shared indicator that preserves spatial continuity, plus a nested, fluid submenu with its own indicator and dual-tier keyboard navigation.

Upstream behaviour
  • Single shared indicator preserves spatial continuity
  • Interactive nested submenu with fluid spring entrance and layout morphing
  • Dual-tier keyboard navigation for parent tabs and sub-items
Differences from upstream
  • React callback props become typed composed CustomEvents; programmatic state assignment emits none.
  • 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.
  • Controlled/uncontrolled React pair maps to initial-value attributes plus owned state and change events.
  • `items` defaults to `[]`; upstream shipped a hard-coded demo list as its default, which is demo data, not API - the preview and example carry it.
  • `default-value`/`default-sub-value` are initial-value attributes (they seed the first render, not re-applied), matching upstream's uncontrolled useState pair; there is no controlled value or programmatic selection setter.
  • Both tiers use tablist/tab with a named submenu and roving tabindex: upstream's role=menu/menuitem, invalid aria-checked and redundant wrapper tabindex are removed (PC-12.6).
  • ArrowDown/ArrowUp open a closed submenu instead of silently changing a hidden sub-selection.
  • `PillSubItem.badge` is declared upstream but never rendered by the source, so it stays in the type and renders nothing.
  • Both pills are single elements driven by the shared helper with the recorded springMorph (main) and springGentle (sub); AnimatePresence becomes a CSS entrance keyframe replayed when the active parent changes.
  • Upstream's dark-only hex palette becomes --detent-color-*, and the submenu outline is raised to 3:1 because its raised surface matches the page in light mode.
Previous: Photo Box Next: PressButton

Docs

Components

Project