Navigation
Scroll Progress Navigation
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-visiblerecorded state; no setterreduced motionrecorded state; no setteractiverecorded 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-idmodepositionscroll-threshold
Properties
sectionscontaineractiveIdmodepositionscrollThresholdprogressactiveSectionfloatingexpandedonSectionClick
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.tsxsha256 2a8a275f4d88…
Native files
The native module and its artifacts:
src/components/scroll-progress-nav/scroll-progress-nav.tssrc/components/scroll-progress-nav/styles.csssrc/components/scroll-progress-nav/define.tsdocs/components/scroll-progress-nav.mdsite/examples/scroll-progress-nav.tstests/browser/scroll-progress-nav.spec.tsevidence/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
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
active-id | activeId | string | undefined | Kebab-case attribute; camelCase property. |
mode | — | — | — | Recorded in the catalog as an attribute. |
position | position | 'top-center' | 'bottom-center' | 'top-right' | 'top-center' | Kebab-case attribute; camelCase property. |
scroll-threshold | scrollThreshold | number | 150 | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
sections | NavSectionItem[] | [...] | 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. |
activeId | string | undefined | Property-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. |
scrollThreshold | number | 150 | Property-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) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-section-click | onSectionClick | (id: string) => void | Composed, 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.