section-routing-engine
v1.0.0
Published
Headless library to synchronize one-page sections with URL routing — React, Vue, and Vanilla JS
Maintainers
Readme
Section Routing Engine
Synchronizes one-page sections with URL routing. Works with React, Vue, and vanilla JS.
- Detects the active section via
IntersectionObserver - Updates the URL on scroll (
#hashor/pathmode) - Scrolls to the right section on direct link / page load
- Supports sticky header offsets
- Respects
prefers-reduced-motionautomatically - Zero UI — bring your own nav
Install
npm install section-routing-engineReact
import { useSectionRouting } from "section-routing-engine/react";
export default function Page() {
const { scrollTo, isActive } = useSectionRouting({
sections: [{ id: "hero" }, { id: "about" }, { id: "contact" }],
offset: 64,
mode: "hash",
});
return (
<>
<nav>
<button onClick={() => scrollTo("hero")}
style={{ fontWeight: isActive("hero") ? 700 : 400 }}>
Hero
</button>
<button onClick={() => scrollTo("about")}
style={{ fontWeight: isActive("about") ? 700 : 400 }}>
About
</button>
<button onClick={() => scrollTo("contact")}
style={{ fontWeight: isActive("contact") ? 700 : 400 }}>
Contact
</button>
</nav>
<section id="hero">...</section>
<section id="about">...</section>
<section id="contact">...</section>
</>
);
}React — imperative API (useSectionRef)
Use useSectionRef when you can't add an id directly — e.g. inside third-party components:
import { useSectionRouting, useSectionRef } from "section-routing-engine/react";
function AboutSection() {
const ref = useSectionRef("about");
return <div ref={ref}>...</div>;
}
function Page() {
const { isActive, scrollTo } = useSectionRouting({ offset: 64 });
// sections are auto-discovered via useSectionRef
}Both APIs can be mixed freely. For Next.js App Router, add "use client" to any component using these hooks.
React — multiple independent instances
useSectionRouting returns a Provider component. Wrap your tree with it to isolate useSectionRef registrations from other instances on the same page:
const { Provider, activeSection, scrollTo } = useSectionRouting({ mode: "hash" });
return (
<Provider>
<Nav />
<main>
<section ref={useSectionRef("hero")}>...</section>
</main>
</Provider>
);Without a Provider, useSectionRef uses the global shared registry (backward-compatible).
Vue
<script setup>
import { useSectionRouting } from "section-routing-engine/vue";
const { scrollTo, isActive } = useSectionRouting({
sections: [{ id: "hero" }, { id: "about" }, { id: "contact" }],
offset: 64,
mode: "hash",
});
</script>
<template>
<nav>
<button @click="scrollTo('hero')"
:style="{ fontWeight: isActive('hero') ? 700 : 400 }">
Hero
</button>
<button @click="scrollTo('about')"
:style="{ fontWeight: isActive('about') ? 700 : 400 }">
About
</button>
<button @click="scrollTo('contact')"
:style="{ fontWeight: isActive('contact') ? 700 : 400 }">
Contact
</button>
</nav>
<section id="hero">...</section>
<section id="about">...</section>
<section id="contact">...</section>
</template>activeSection is a Vue Ref<string | null>. Isolation works automatically via Vue's provide/inject — no explicit Provider needed.
Vue — imperative API (useSectionRef)
<script setup>
import { useSectionRef } from "section-routing-engine/vue";
const sectionRef = useSectionRef("about");
</script>
<template>
<div :ref="sectionRef">...</div>
</template>Vanilla JS
import { createSectionRouter } from "section-routing-engine/vanilla";
const router = createSectionRouter({
offset: 64,
mode: "hash",
});
router.observe("hero", document.getElementById("hero")!);
router.observe("about", document.getElementById("about")!);
router.observe("contact", document.getElementById("contact")!);
router.onActiveChange((id) => {
document.querySelectorAll("nav a").forEach((el) => {
el.classList.toggle("active", el.dataset.section === id);
});
});
// Scroll programmatically
document.querySelector("[data-section='about']")
?.addEventListener("click", () => router.scrollTo("about"));
// Clean up when done
// router.destroy();Options
All three APIs share the same options:
| Option | Type | Default | Description |
|---|---|---|---|
| sections | SectionConfig[] | [] | Declarative section list |
| offset | number | 0 | Pixels subtracted from scroll target (sticky header height) |
| mode | "hash" \| "path" | "hash" | URL strategy |
| rootMargin | string | "0px" | Global IntersectionObserver rootMargin, e.g. "-20% 0px" |
| scrollBehavior | ScrollBehavior | "smooth" | Scroll animation — "smooth", "instant", or "auto" |
SectionConfig
| Field | Type | Description |
|---|---|---|
| id | string | Must match the element's id attribute |
| rootMargin | string? | Per-section rootMargin override |
useSectionRef options
useSectionRef(id, { rootMargin? }) accepts an optional second argument to override the rootMargin for that specific section:
// React
const ref = useSectionRef("about", { rootMargin: "-30% 0px" });
// Vue
const sectionRef = useSectionRef("about", { rootMargin: "-30% 0px" });Return value (React / Vue)
| Property | Type | Description |
|---|---|---|
| activeSection | string \| null (Vue: Ref) | ID of the currently visible section |
| scrollTo | (id: string) => void | Scroll to a section and update the URL |
| isActive | (id: string) => boolean | Convenience helper for nav highlighting |
| Provider | React.ComponentType | (React only) Scope useSectionRef to this instance |
Return value (Vanilla)
| Property / Method | Description |
|---|---|
| observe(id, el, rootMargin?) | Start watching an element |
| unobserve(id) | Stop watching a section |
| scrollTo(id) | Scroll to a section and update the URL |
| onActiveChange(cb) | Subscribe to changes — returns unsubscribe function |
| activeSection | Currently active section ID (getter) |
| destroy() | Disconnect all observers and listeners |
prefers-reduced-motion
scrollTo automatically switches to instant scrolling when the user has enabled "Reduce motion" in their OS accessibility settings. No configuration needed.
Imports
React is the default — both of these are identical:
import { useSectionRouting } from "section-routing-engine";
import { useSectionRouting } from "section-routing-engine/react";Vue and vanilla require the explicit subpath:
import { useSectionRouting } from "section-routing-engine/vue";
import { createSectionRouter } from "section-routing-engine/vanilla";License
MIT © 2026 Peter R. Stuhlmann
