hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Head Management
Active Links
Signals
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
Server Caller
View Transitions
Middleware
Session Channels
CSRF Protection
CLI
Vite Config
Project Structure
Styling
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Listbox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition

Active Links#

Highlighting the current page in navigation needs one question answered: does the current URL match a given route? useRouteActive, useRouteMatch, and <NavLink> answer it against the same route grammar your route table uses, so /posts/:id matches any post and hands you the id.

useRouteActive#

Returns true when the current path matches the route.

import { useRouteActive } from 'hono-preact';

function Tab() {
  const active = useRouteActive('/docs');
  return (
    <a href="/docs" aria-current={active ? 'page' : undefined}>
      Docs
    </a>
  );
}

By default the match is exact. Pass { exact: false } to also match descendant paths, e.g. highlight a section for every page beneath it:

// active on /docs/components AND /docs/components/dialog
const inSection = useRouteActive('/docs/components', { exact: false });

One caveat: because every path is a descendant of /, a non-exact match on the root (useRouteActive('/', { exact: false })) is active everywhere. Keep a "Home" link exact (the default).

useRouteMatch#

Same matching, but returns the captured params (or null). Use it when you want to both test and read the dynamic segments.

import { useRouteMatch } from 'hono-preact';

function Crumb() {
  const params = useRouteMatch('/posts/:id');
  // on /posts/42 -> { id: '42' }, elsewhere -> null
  return params ? <span>Post {params.id}</span> : null;
}

The route accepts the full pattern grammar: :param, *, +, :param?.

buildPath#

Build a typed URL from a route pattern and its params. TypeScript enforces that the pattern is registered and that the params object matches the pattern's dynamic segments. Param values must be strings; convert numbers or other types before passing them.

import { buildPath } from 'hono-preact';

// /posts/42
const href = buildPath('/posts/:id', { id: '42' });

// /dashboard  (no params needed; second argument is omitted)
const dashHref = buildPath('/dashboard');

buildPath is the safe alternative to template literals: a wrong key or a missing param is a compile error, not a silent bug.

For a rest param (:rest*, :rest+), pass string[] to build real slash-separated segments: buildPath('/docs/:slug+', { slug: ['a', 'b'] }) builds /docs/a/b, encoding each entry individually. A plain string is still accepted and is encoded whole, so embedded / characters become %2F rather than additional segments.

A <a> that applies an active or inactive class for you (and sets aria-current="page" when active).

import { NavLink } from 'hono-preact';

<NavLink
  href="/docs"
  activeClass="text-accent font-semibold"
  inactiveClass="text-muted"
>
  Docs
</NavLink>;

Any class you pass is always applied; activeClass / inactiveClass are merged on top per state. Other anchor props (target, rel, data-*) pass straight through.

Pass your own aria-current to override the computed value, including aria-current={undefined} to opt out entirely (the link still gets its active class, just no aria-current attribute). Active state is path-only: it cannot see the query string, so if a set of links differ only by their query (e.g. ?tab=), manage aria-current yourself.

Use exact={false} for section links, and match when the link target differs from the pattern you want to highlight on:

// links to /posts, but stays active on /posts/123
<NavLink href="/posts" match="/posts/:id" activeClass="text-accent">
  Posts
</NavLink>

Pair prefetch with prefetchLoaders to warm a link's target route before the click, so the navigation that follows has no loading state at all:

<NavLink href="/movies" prefetch="hover" prefetchLoaders={movieList}>
  Movies
</NavLink>

Two trigger strategies:

  • 'hover': warms on hover intent (a short debounce on pointerenter, cancelled if the pointer leaves before the delay elapses) and on focus, so keyboard navigation gets the same warming as a mouse.
  • 'visible': warms once the link scrolls into the viewport, then stops watching. Use it for links a user is likely to click without ever hovering, such as a card in a list.

prefetchLoaders names the loaders to warm explicitly, either a single LoaderRef or an array of them; there is no auto-discovery from href. Both props are additive: they compose with activeClass / inactiveClass, match, and exact with no interaction between them. See Link Prefetch for the underlying usePrefetch hook this wraps.

useNavigate() returns a navigate(path, options?) function for navigating from an event handler (a logout button, a post-action redirect). A soft navigate runs the same client middleware, loaders, and view transitions as a <NavLink> click.

import { useNavigate } from 'hono-preact';

function LogoutButton() {
  const navigate = useNavigate();
  return (
    <button onClick={() => navigate('/login', { replace: true })}>
      Log out
    </button>
  );
}

A fragment target is a same-document anchor navigation: navigate('#usage') writes the hash to the URL and scrolls to the matching element, like a native anchor click. No route change happens, so nothing animates and the transition option has no effect.

OptionTypeDefaultDescription
replacebooleanfalseReplace the current history entry instead of pushing a new one.
reloadbooleanfalseDo a full-page navigation (clean slate) instead of a client navigation.
transitionbooleantrueSet false to navigate without a view transition.

API reference#

<NavLink> forwards any other <a> attribute (e.g. target, rel, data-*) to the underlying anchor and sets aria-current="page" while the link is active. A caller-supplied aria-current always wins, and aria-current={undefined} suppresses the computed value. Because active detection is path-only, links that differ only by query string need their aria-current managed explicitly.

PropTypeDefaultDescription
hrefstringrequiredDestination path.
matchstringhrefPattern tested for the active state.
exactbooleantrueMatch the full path; set false for prefix (section) matching.
classstringnoneAlways applied.
activeClassstringnoneMerged in when the link is active.
inactiveClassstringnoneMerged in when the link is not active.
transitionbooleantrueSet false to navigate without a view transition.
prefetch'hover' | 'visible' | falsefalseTrigger for warming prefetchLoaders. No trigger means no prefetching.
prefetchLoadersLoaderRef | ReadonlyArray<LoaderRef>noneLoaders to prefetch for this link's target route. Required for prefetch to do anything.

See also#