Live demos:

use:link — SPA navigation

The use:link action turns a regular <a> into an SPA link: it intercepts the click, prevents a full page reload, and calls push() internally. Modifier keys (Ctrl/Cmd-click, middle-click) and target="_blank" are respected — the browser still handles those.

Basic use:link
 

For programmatic navigation, named routes, and the array/object call forms, see Named Routes and Programmatic Navigation.

use:active — adding a CSS class to the matching link

use:active adds a CSS class to the element whenever its configured path matches the current location. With no argument, the path defaults to the link's href and the class is active.

Default and custom class
 
How it works: use:active listens to hashchange and popstate directly on window. It is NOT a reactive store — you can't $derived off it. Read location() if you need that in JS.

Explicit path — string prefix or regex

Pass an explicit path to match something other than the link's own href. A string is run through regexparam, so you get the same :param and * syntax used in route definitions.

Explicit path patterns
 

Wildcard quirk: /docs/* matches descendants only

The string pattern /foo/* does NOT match the bare path /foo. It only matches descendants like /foo/anything.

regexparam compiles /foo/* to /^\/foo\/(.*)\/?$/i — the trailing slash after foo is mandatory. So a sidebar parent using use:active={'/foo/*'} goes dark the moment the user lands on the bare /foo index page.

Why /foo/* misses /foo
 

Use this deliberately when you want a link UNHIGHLIGHTED on the index page and only lit on descendants — that's a legitimate UX choice. For the common "parent stays lit on its own index AND on every child" case, see the next section.

Branch matching — bare path + descendants

For sidebar parents that should stay highlighted on /docs AND every child, you have three equivalent options.

1. subtree option (recommended)

No path string, no regex. The action reads the link's href and internally registers both an exact match AND a /docs/* descendants pattern. Best fit when the href is a variable in a generated nav.

Branch active via subtree option
 

2. Regex form

Useful when you can't or don't want to depend on the link's href.

Branch active via regex
 

3. Stack two use:active calls (long form of subtree)

The default action catches the bare path; the prefix action catches descendants. Both add the same active class. Library aggregates per (node, className) so the two actions cooperate instead of fighting.

Branch active via stacked actions
 

All three yield:

  • /docs → active
  • /docs/guide → active
  • /docs/api/intro → active
  • /about → not active

Sidebar with submenu

Parent stays active on its own index page AND when any child route is open; children only highlight on their exact path.

Sidebar with submenu
 

Behavior across URLs:

Current URLParent "Users"Submenu "All users"Submenu "Create user"
/usersactiveactive
/users/newactiveactive
/users/123active
/

Two-class pattern — distinguish "really active" from "parent of active"

Style the link that exactly matches the current URL differently from a parent whose descendant is active. The cleanest way is the subtree + subtreeClassName pair — one action call covers both cases.

Two-class parent/child via subtree
 
Long form: stacked use:active calls (equivalent)
Two-class parent/child (long form)
 

Behavior across URLs:

Current URLParent "Users"Child "All users"Child "Create"Child "User 123"
/userslink-active (red)link-active
/users/newsublink-active (orange)link-active
/users/123sublink-activelink-active
/about
Why this works: the regexparam wildcard quirk is on your side here. The exact-match action only matches the bare /users; the /users/* action only matches descendants. The two classes never collide, so the parent is unambiguously either "really active" or "branch active" — never both.

Three-level menu

Repeat the pattern at each ancestor depth.

Three-level menu
 

On /admin/users/123: grandparent and parent both get sublink-active, the leaf gets link-active.

Generating nav from a route tree

Once you can decide "parent or leaf?" at render time, you can drive the entire sidebar from a single data structure — the same one you pass to createHierarchy() for route registration.

A reusable NavLink helper

Wraps use:link + use:active so the menu walker stays small. Pass subtree={true} for parent nodes; default behavior for leaves.

NavLink helper component
 

Walk the tree to render the menu

Tree walker
 

Same tree drives the routes too

Route registration from the same tree
 
Live demo: Open /nav-tree-demo — see example/src/routes/NavTreeDemo.svelte and example/src/routes/nav-tree.js for the full source.

Permission-filtered nav (helpers/nav-tree)

The filterByPermissions(tree) helper from @keenmate/svelte-spa-router/helpers/nav-tree walks a route tree and either hides nodes the current user can't reach (default) or keeps them in place with a _forbidden flag for styled disabled rendering.

Two modes

filterByPermissions modes
 

NavTree node schema

Per-node options
 

Naming note: the boolean fields use bare HTML-attribute names (hidden, disabled) to align with the KeenMate web-components convention. The helper predicate isNodeHidden(node) keeps its is* prefix because it's a function, not a field.

hidden — static boolean or reactive getter

Either form works. The getter is called every filter pass, so anything it reads — environment variables, $state, feature flags — participates in the reactive update chain automatically.

hidden patterns
 

disabled — "coming soon" placeholder

disabled: true marks a node as a product-level placeholder — "coming soon", "in private beta", "Q3 roadmap". It renders forbidden in both filter modes (it never simply disappears in hide mode like hidden does), because the intent is to advertise the item, not gate it on user permissions. Ancestor permission denial still cascades — you can't see a placeholder in a section you can't enter.

Pair with the new disabledClassName filter option to style placeholders distinctly from permission-denied items (amber "unavailable" vs. red "forbidden"). When a node is both disabled AND permission-denied (rare combo), disabledClassName wins — the product-level signal is the more permanent one.

Hierarchical permission inheritance

When inheritPermissions: true (default), child accessibility requires every ancestor's permission check to also pass. The filter chains sequential hasPermission() calls — one per level — which matches how the router's own hierarchical-mode pipeline checks routes. Set inheritPermissions: false if your nav structure doesn't mirror an access hierarchy.

Behavior matrix

Node stateMode 'hide'Mode 'disable'
hidden: true or getter returns truegonegone (same — explicit "never in nav")
User has accessshownshown
User lacks accessgoneshown + _forbidden: true + .forbidden class
disabled: true (product-level placeholder)shown + _forbidden: true + .unavailable class*shown + _forbidden: true + .unavailable class*
All visible children forbiddenparent goneparent forbidden (cascading; keeps .forbidden)

* When disabledClassName is set on the filter options. Defaults to forbiddenClassName if omitted (backward compatible).

Reactivity

filterByPermissions() calls hasPermission() internally, which reads through the reactive user state. Calling the filter inside a $derived makes the entire sidebar update live when:

  • setCurrentUser() fires (login, logout, role change, websocket permission update)
  • Any hidden getter reads $state that mutates
  • Any inherited reactive state changes

No subscription wiring, no manual invalidation — same model as the rest of the runes-based router.

NavLink — forbidden rendering

When the filter returns a node with _forbidden: true, NavLink renders a non-interactive <span> with the configured class and aria-disabled="true" instead of an <a>. No use:link, no navigation, no use:active.

NavLink with forbidden state
 
Live demo: /nav-tree-demo ships with a user toggle (Donna with limited permissions ↔ Audrey with full access) and a mode toggle (hide ↔ disable) — flip both and watch the sidebar re-render live, with no page reload.

inactiveClassName — class while NOT matching

The options form also accepts inactiveClassName for a class that's applied while the pattern does NOT match. Useful for muted/disabled visual states.

Inactive class
 

When multiple use:active actions share the same inactiveClassName, the class is present only when NO controlling entry matches — the element is treated as "fully inactive."

Pitfalls

  • Path strings must start with / or *. Anything else throws — internal locations always start with /, so a path like 'foo' could never match.
  • The pattern is compiled once at mount. Changing opts.path reactively won't re-parse — Svelte would need to tear the element down and remount it.
  • use:active is browser-only. The module has a typeof window !== 'undefined' guard for SSR.
  • Don't use use:link on external URLs (use a plain <a>). The action would attempt SPA navigation and break.