Skip to content

Free MIT template · Day 6 of 365

You're looking at a demo. Download the built static files (drop on any host) or grab the full source to customize.

↓ Download static ↓ Source (Astro) All 365 →Build with me →

# Navigation

routekit gives you three ways to change URLs: native `<a>` clicks (intercepted automatically), programmatic `router.go()`, and direct `history.pushState` if you want to bypass routekit’s hooks.

## Link interception

When you call `router.start()`, routekit attaches a single delegated click handler at `document`. Any in-app `<a href="...">` click that:

- has a same-origin URL,
- is not a download (`target="_blank"`, `download`, `[rel="external"]`),
- is not a modified click (cmd-click, middle-click),

is intercepted: routekit calls `event.preventDefault()` and `router.go(href)` instead. Native browser behavior is preserved otherwise.

    <!-- Just write normal HTML; routekit handles the rest --><a href="/posts/42">Read post 42</a><a href="/posts.csv" download>Export</a>            <!-- bypassed, native --><a href="https://example.com">External</a>          <!-- bypassed, native --><a href="/edit" target="_blank">Edit (new tab)</a>  <!-- bypassed, native -->

Caution

Click interception is opt-out, not opt-in. If you have a router-managed `<a>` that you want to behave natively, give it `[data-routekit-bypass]`:

    <a href="/legal/terms" data-routekit-bypass>Terms</a>

## Programmatic navigation

    router.go('/posts/42');             // pushes a new history entryrouter.replace('/login');           // replaces current history entryrouter.back();                      // history.back()router.forward();                   // history.forward()

`go()` and `replace()` accept either a path string or an object:

    router.go('/posts/42');router.go({ path: '/posts/42', state: { from: 'newsletter' } });

The `state` is exposed on subsequent `match.state` reads. Use this for back-button-friendly flows (e.g. preserving scroll position, modal open/close state).

## Active-link styling

There’s no `<NavLink>` component — instead, attach a CSS class to your `<a>` element and let the browser do the work:

    import { isActive } from 'routekit';
    document.querySelectorAll('a[href]').forEach((a) => {  a.toggleAttribute('data-active', isActive(a.getAttribute('href')!, router.url));});

Then style with `[data-active]`:

    nav a[data-active] {  color: var(--accent);  font-weight: 600;}

`isActive(href, currentUrl)` returns `true` when:

- exact match (`/users` → `/users`), OR
- prefix match with a trailing slash (`/users/` → `/users/42`).

If you want stricter matching (exact only), pass `{ exact: true }`:

    isActive('/users', router.url, { exact: true });

## Handling external links

Cross-origin `<a>` clicks are not intercepted — they trigger normal browser navigation away from your app. If you want to confirm before leaving (for unsaved-form prompts, e.g.), use the standard `beforeunload` event:

    window.addEventListener('beforeunload', (e) => {  if (formIsDirty) {    e.preventDefault();    e.returnValue = '';  }});

routekit deliberately doesn’t expose a “before-navigate” hook. The platform already has one.

## Scroll restoration

routekit doesn’t manage scroll. By default, `router.go()` does NOT scroll to the top — your app keeps its current scroll position. To match the typical SPA expectation:

    router.on('navigate', ({ trigger }) => {  if (trigger === 'push') {    // Forward navigation: scroll to top.    window.scrollTo({ top: 0, behavior: 'instant' });  }  // 'pop' (back/forward button): browser handles scroll restoration.});

The `trigger` field disambiguates pushed vs. popped navigations so you only scroll on forward moves.