# One Page Navigation Widget

> Floating dot / pill navigation for single-page sites. Scrolling to a section auto-highlights the matching nav item. Clicking a nav item smooth-scrolls to that section.

**Class file:** `includes/Elements/One_Page_Navigation.php` (786 lines)
**Slug:** `one-page-navigation` (widget id `eael-one-page-navigation`)
**Public docs:** <https://essential-addons.com/elementor/docs/one-page-navigation/>
**Pro-shared:** Pro-only.

## Overview

Single-page-site navigation overlay. User defines a Repeater of "anchors" — each row has a label + target CSS selector (`#section-id`). At runtime, JS listens to scroll position and highlights the matching nav item; clicking a nav item smooth-scrolls to the target section. Position is fixed (left / right / top / bottom of viewport).

## Pro vs Lite

Pro-only.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/One_Page_Navigation.php` | Widget class (786 lines) |
| `src/css/view/one-page-navigation.scss` → `assets/front-end/css/view/one-page-navigation.min.css` | Nav styling (dot / pill / line variants) |
| `src/js/view/one-page-navigation.js` → `assets/front-end/js/view/one-page-navigation.min.js` | Scroll position detection + smooth-scroll |
| `config.php` entry `'one-page-navigation'` | Self CSS + self JS |

## Architecture

- **Composes Lite's `Helper`** (line 13).
- **No vendor libs** — uses native `scroll` event + IntersectionObserver (or `getBoundingClientRect`) for active-section detection. Smooth-scroll via CSS `scroll-behavior: smooth` or JS `window.scrollTo({behavior: 'smooth'})`.
- **Anchor Repeater** — each row: label + target selector. Pro doesn't auto-detect sections; user enters CSS selectors manually.
- **Fixed-position container** — `position: fixed` with configurable edge (left / right / top / bottom).
- **Multiple style variants** — dot / pill / line / number / etc. — CSS-driven.

## Render Output

```html
<nav class="eael-one-page-navigation eael-opn-{position} eael-opn-style-{style}">
  <ul>
    <li class="eael-opn-item active" data-target="#section-1">
      <span class="indicator"></span>
      <span class="label">{label}</span>
    </li>
    <li class="eael-opn-item" data-target="#section-2">...</li>
    ...
  </ul>
</nav>
```

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| Anchors Repeater | Content → Anchors | REPEATER | Per-anchor label + selector |
| (Per-anchor) label | Inside Repeater | TEXT | Display name |
| (Per-anchor) target | Inside Repeater | TEXT | CSS selector (`#hero`, `.about`, etc.) |
| Position | Content → Position | CHOOSE | `left` / `right` / `top` / `bottom` |
| Style variant | Content → Style | SELECT | `dot` / `pill` / `line` / `number` / etc. |
| Show labels | Content → Labels | SWITCHER | Label visibility / on-hover |
| Scroll offset | Content → Behavior | NUMBER | px offset for sticky-header compensation |
| Smooth scroll | Content → Behavior | SWITCHER | Animate vs jump |
| Per-state styling (active / hover / normal) | Style → Items | various | Per-state styling |

## Conditional Dependencies

```text
show_labels = 'on-hover' → labels appear only on hover
position = 'top' | 'bottom' → horizontal layout
position = 'left' | 'right' → vertical layout
```

## JavaScript Lifecycle

```js
var sections = anchors.map( a => document.querySelector( a.target ) );
window.addEventListener( 'scroll', function() {
    var active = sections.findIndex( s =>
        s.getBoundingClientRect().top <= scrollOffset
    );
    items.forEach( ( item, i ) => item.classList.toggle( 'active', i === active ) );
} );

items.forEach( item => item.addEventListener( 'click', function() {
    var target = document.querySelector( item.dataset.target );
    target.scrollIntoView( { behavior: 'smooth', block: 'start' } );
} ) );
```

## Hooks & Filters

Standard widget render.

## Common Issues

| Symptom | Cause | Fix |
| --- | --- | --- |
| Active state never updates | Scroll event not bound or selector wrong | Verify each anchor's selector resolves to a real DOM element |
| Smooth scroll jumps under sticky header | Scroll-offset not set | Use `scroll_offset` control to match header height |
| Multiple instances conflict | All instances bind scroll listener globally | Verify each instance scopes its handler |
| Nav overlaps content on mobile | No mobile-hide control | Use Elementor's responsive visibility settings |

## Known Limitations

- **Manual selector entry** — no auto-discover-sections. User must remember each section's `#id`
- **`prefers-reduced-motion` not honoured** — smooth scroll runs regardless
- **No keyboard accessibility** — tab navigation past nav doesn't visually highlight active item
- **Scroll event not throttled** — large pages with many sections recompute active state on every scroll frame
- **Mobile UX** — fixed nav can overlap content; user has to hide manually

## Cross-References

- Sibling: [`offcanvas.md`](offcanvas.md) (alternative nav pattern), [`advanced-menu.md`](advanced-menu.md) (traditional nav)
- Shared patterns: [`_patterns.md`](_patterns.md)
