Detent web components
By Shane Logsdon

Navigation

Scroll Progress Navigation

Implemented

Floating progress navigation pill tracking scroll depth and active document headings

detent-scroll-progress-nav

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

7.4 KB gzip for the class module

Variants

The pill is shown inline here so it stays inside the preview: mode="fixed" is the default and pins it to the viewport. The horizontal progress line (aria-hidden) follows document scroll through a passive, frame-coalesced listener; the current section carries aria-current="location"; a click emits detent-section-click with { id } and scrolls to the section, without animation under reduced motion. Below 768px the row collapses to a compact menu with an aria-expanded toggle.

Recorded states

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

Usage

Import

import '/components/scroll-progress-nav/define.js';

// or register it yourself
import { DetentScrollProgressNav } from '/components/scroll-progress-nav/scroll-progress-nav.js';

Attributes

  • active-id
  • mode
  • position
  • scroll-threshold

Properties

  • sections
  • container
  • activeId
  • mode
  • position
  • scrollThreshold
  • progress
  • activeSection
  • floating
  • expanded
  • onSectionClick

Slots

No slots are recorded for this component.

Events

  • detent-section-click — { id: string }

Example

Runnable example: site/examples/scroll-progress-nav.ts. The snippet below is generated from the catalogued attributes.

<detent-scroll-progress-nav
  active-id
  mode
  position="top-center"
  scroll-threshold="150"
></detent-scroll-progress-nav>

Property-only values

const element = document.querySelector('detent-scroll-progress-nav');
if (element !== null) {
  // sections: NavSectionItem[]
  element.sections = [];
  // onSectionClick: (id: string) => void
  element.onSectionClick = () => {};
}

Source

The pinned upstream source for this row:

  • src/components/ui/ScrollProgressNav.tsx sha256 2a8a275f4d88…

Native files

The native module and its artifacts:

  • src/components/scroll-progress-nav/scroll-progress-nav.ts
  • src/components/scroll-progress-nav/styles.css
  • src/components/scroll-progress-nav/define.ts
  • docs/components/scroll-progress-nav.md
  • site/examples/scroll-progress-nav.ts
  • tests/browser/scroll-progress-nav.spec.ts
  • evidence/components/scroll-progress-nav.md

API

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

Attributes

Attributes
AttributePropertyTypeDefaultNotes
active-idactiveIdstringundefinedKebab-case attribute; camelCase property.
mode———Recorded in the catalog as an attribute.
positionposition'top-center' | 'bottom-center' | 'top-right''top-center'Kebab-case attribute; camelCase property.
scroll-thresholdscrollThresholdnumber150Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
sectionsNavSectionItem[][...]Property-only: assign from script; not accepted as an attribute.
container——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
activeIdstringundefinedProperty-only: assign from script; not accepted as an attribute.
mode——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
position'top-center' | 'bottom-center' | 'top-right''top-center'Property-only: assign from script; not accepted as an attribute.
scrollThresholdnumber150Property-only: assign from script; not accepted as an attribute.
progress——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
activeSection——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
floating——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
expanded——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
onSectionClick(id: string) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-section-clickonSectionClick(id: 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.
  • `containerRef` becomes the `container` element property (property-only, no attribute form); `mode` is pinned from source (`fixed` default, `inline`, `sticky`) though the props table omits it.
  • The scroll handler is passive, rAF-coalesced and cancelled on disconnect; jumps honour reduced motion (`behavior: auto`).
  • The pill glides on springMorph (220/32/1) via the shared helper (position and size on one progress spring; the nav container's layout size morph is not reproduced); the index fallback is `String(i + 1).padStart(2, "0")` (upstream produced "010"); the compact menu is a media-query layout with an `expanded` state instead of AnimatePresence.

What it does

A floating table-of-contents pill with a decorative scroll-progress line, a scroll spy, a compact mobile menu and smooth, reduced-motion-aware section jumps.

Upstream behaviour
  • Real-time document scroll progress bar integrated along the pill boundary
  • Shared layout pill indicator smoothly moving between active headings
  • Seamless transition from static page banner to compact floating island
  • Smooth scroll anchoring with customizable offset threshold
  • Mobile-optimized responsive compact menu preventing content obstruction
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.
  • `containerRef` becomes the `container` element property (property-only, no attribute form); `mode` is pinned from source (`fixed` default, `inline`, `sticky`) though the props table omits it.
  • The scroll handler is passive, rAF-coalesced and cancelled on disconnect; jumps honour reduced motion (`behavior: auto`).
  • The pill glides on springMorph (220/32/1) via the shared helper (position and size on one progress spring; the nav container's layout size morph is not reproduced); the index fallback is `String(i + 1).padStart(2, "0")` (upstream produced "010"); the compact menu is a media-query layout with an `expanded` state instead of AnimatePresence.
Previous: Confetti Burst Next: Scroll Velocity Text

Docs

Components

Project