README.md

July 18, 2026 ยท View on GitHub

Winner of the KaiCode 2026 Open Source Festival โ€” first place, Perfect tier

yew-nav-link

Enterprise-grade navigation library for Yew โ€” automatic active state detection and a complete component system.

๐ŸŒ Live demo โ†’ โ€” ๐Ÿ“– Architectural book โ†’

CI Pages Mutants Crates.io docs.rs Downloads MSRV License: MIT REUSE OSSF Scorecard Codecov

Animated demo: clicking the Yew, Nav and Link tabs changes the URL path and page content while the active tab is highlighted automatically

Click a NavLink โ€” the route changes, and the active class plus aria-current="page" follow automatically.


Table of Contents


Overview

yew-nav-link is a comprehensive navigation library for the Yew web framework. It provides:

FeatureDescription
NavLinkDrop-in replacement for Yew Router's <Link> with automatic active class detection
Component System15+ ready-to-use UI components (tabs, dropdowns, pagination, badges, icons)
HooksReactive hooks for route state, active checking, breadcrumbs, and programmatic navigation
UtilitiesPath manipulation, URL encoding, keyboard navigation, and query string handling
CustomizationCustom CSS classes, programmatic navigation, and extensible breadcrumb providers

The core NavLink component eliminates manual active state tracking. It compares the current route against the target on every render and applies the active CSS class automatically โ€” zero configuration required.

(back to top)


Quality Gates

Every gate below runs in CI and blocks merge on failure โ€” no advisory-only checks. This table is the fast path for reviewers: each quality dimension maps to the tool that enforces it and the workflow that runs it.

DimensionEnforced byWorkflow
Unit + doctestscargo nextest + cargo test --doc, across MSRV / stable / nightly on Linux, macOS, Windowsci.yml ยท test, check
Browser testswasm-bindgen-test headless on Chrome and Firefoxci.yml ยท wasm_tests
End-to-end testsPlaywright against the trunk-built demo (Chromium + Firefox)ci.yml ยท e2e
Property testingproptest over path / URL / query invariantsci.yml ยท test
Fuzzingcargo-fuzz, 5 targets (path join/normalize, URL round-trip, query params, URL parts)fuzz.yml
Mutation testingcargo-mutants: full run nightly + diff-scoped pass per PRmutants.yml
Coverage controlcargo-llvm-cov โ†’ Codecov, 95 % project + patch gateci.yml ยท coverage
Static analysisclippy -D warnings (all + no-default features) ยท CodeQL SAST ยท zizmor over the workflows themselvesci.yml ยท check ยท codeql.yml ยท zizmor.yml
Supply chaincargo-deny, cargo-audit, cargo-machete, cargo-udepsci.yml ยท security
Security postureOSSF Scorecardscorecard.yml
API stabilitycargo-semver-checks + cargo-public-api baseline diffci.yml ยท semver_checks, public_api
Releases & semverrelease-plz (version + changelog + publish) ยท signed build provenance & SBOM attestationrelease-plz.yml ยท release-attestations.yml
LicensingREUSE / SPDX complianceci.yml ยท reuse
Performance budgetscriterion benches ยท WASM size budget ยท Lighthouse assertionsci.yml ยท benchmarks, wasm-build, lighthouse
Formatting & workflowscargo +nightly fmt --check ยท actionlint ยท PR-title format gateci.yml ยท fmt, actionlint, pr_title

(back to top)


Installation

[dependencies]
yew-nav-link = "0.12"

(back to top)

Requirements

DependencyVersion
Rust1.96+
Edition2024
Yew0.23+
yew-router0.20+

(back to top)


Quick Start

Component Syntax

use yew::prelude::*;
use yew_nav_link::NavLink;
use yew_router::prelude::*;

#[derive(Clone, PartialEq, Routable)]
enum Route {
    #[at("/")]
    Home,
    #[at("/about")]
    About,
}

#[component]
fn Navigation() -> Html {
    html! {
        <nav>
            <NavLink<Route> to={Route::Home}>{ "Home" }</NavLink<Route>>
            <NavLink<Route> to={Route::About}>{ "About" }</NavLink<Route>>
        </nav>
    }
}

When the user visits /about, the second link automatically receives class="nav-link active".

Function Syntax

use yew::prelude::*;
use yew_nav_link::{nav_link, Match};
use yew_router::prelude::*;

#[component]
fn Menu() -> Html {
    html! {
        <nav>
            { nav_link(Route::Home, "Home", Match::Exact) }
            { nav_link(Route::Docs, "Docs", Match::Partial) }
        </nav>
    }
}

Partial Matching

Keep parent links highlighted on nested routes:

html! {
    <nav>
        // Active on /docs, /docs/api, /docs/anything
        <NavLink<Route> to={Route::Docs} partial=true>{ "Docs" }</NavLink<Route>>
    </nav>
}

Partial matching is segment-aware: /docs matches /docs/api but not /documentation.

Custom CSS Classes

Customize the default nav-link and active classes:

html! {
    <nav>
        // Custom base class
        <NavLink<Route> to={Route::Home} class="menu-item">{ "Home" }</NavLink<Route>>
        
        // Custom active class
        <NavLink<Route> to={Route::About} active_class="is-selected">{ "About" }</NavLink<Route>>
        
        // Both custom
        <NavLink<Route> to={Route::Contact} class="sidebar-link" active_class="highlighted">{ "Contact" }</NavLink<Route>>
    </nav>
}

Programmatic Navigation

use_navigation::<R>() returns a [Navigation<R>] handle exposing pre-built Callbacks โ€” no manual Callback::from(...) boilerplate.

use yew::prelude::*;
use yew_nav_link::use_navigation;

#[component]
fn MyComponent() -> Html {
    let nav = use_navigation::<Route>();

    html! {
        <>
            // Push a new entry onto the history stack.
            <button onclick={nav.push_callback(Route::About).reform(|_: MouseEvent| ())}>
                { "Go to About" }
            </button>

            // Replace the current entry without growing history.
            <button onclick={nav.replace_callback(Route::Home).reform(|_: MouseEvent| ())}>
                { "Replace with Home" }
            </button>

            // Browser back / forward.
            <button onclick={nav.go_back.reform(|_: MouseEvent| ())}>{ "Back" }</button>
            <button onclick={nav.go_forward.reform(|_: MouseEvent| ())}>{ "Forward" }</button>
        </>
    }
}

Custom Breadcrumb Providers

Implement [BreadcrumbLabelProvider] to control how each path segment is rendered. The provider operates on paths (e.g. /docs/api), not on Routable enum variants โ€” it works the same for static and parameterised routes.

use std::rc::Rc;
use yew_nav_link::{BreadcrumbLabelProvider, use_breadcrumbs};

struct MyLabels;

impl BreadcrumbLabelProvider for MyLabels {
    fn label_for_path(&self, path: &str) -> String {
        match path {
            "/" => "Home".into(),
            "/about" => "About us".into(),
            p if p.starts_with("/users/") => format!("User {}", &p[7..]),
            other => other.into(),
        }
    }
}

#[component]
fn Crumbs() -> Html {
    // Provide the implementation through context (omitted for brevity);
    // then read the trail.
    let trail = use_breadcrumbs::<Route>();

    html! {
        <nav aria-label="Breadcrumb">
            { for trail.into_iter().map(|item| html! {
                <span aria-current={if item.is_active { "page" } else { "" }}>
                    { item.label }
                </span>
            }) }
        </nav>
    }
}

(back to top)

Components

Core Navigation

ComponentPurpose
NavLink<R>Navigation link with automatic active state
[NavList]Accessible navigation list container (<ul> with ARIA)
[NavItem]Navigation list item (<li>)
[NavDivider]Visual separator between navigation groups

UI Components

ComponentPurpose
[NavBadge]Badge/counter for navigation items
[NavHeader]Section header for navigation groups
[NavText]Plain text element within navigation
[NavIcon]Icon with configurable size
[NavLinkWithIcon]Link with integrated icon
[NavDropdown]Dropdown menu with items and dividers
[NavTabs]Tabbed navigation container
[NavTab]Individual tab with active state
[NavTabPanel]Content panel for tabs
[Pagination]Page navigation controls
[PageItem]Individual page indicator
[PageLink]Clickable page link

Hooks

HookReturnsDescription
use_route_info::<R>()Option<R>Currently matched route, or None when nothing matches
use_is_active(route)boolWhether the given route is currently active
use_is_exact_active(route)boolWhether the route matches exactly
use_is_partial_active(route)boolWhether the route is a prefix of the current path
use_breadcrumbs::<R>()Vec<BreadcrumbItem<R>>Auto-generated breadcrumb trail from current route
use_navigation::<R>()Navigation<R>Programmatic navigation (push, replace, go back/forward)
use_query_params()QueryParamsURL query parameters (multi-value utils::QueryParams)

Utilities

Path helpers are re-exported at the crate root; the URL and keyboard helpers live under the utils module (yew_nav_link::utils::โ€ฆ).

FunctionPathDescription
is_absolute(path)crate rootCheck if a path starts with /
join_paths(a, b)crate rootJoin two path segments safely
normalize_path(path)crate rootCollapse duplicate slashes and resolve ./..; a single trailing slash is preserved
urlencoding_encode(s)utils::Percent-encode a string for URLs
urlencoding_decode(s)utils::Decode a percent-encoded string, + becomes a space (None on invalid UTF-8)
percent_decode(s)utils::Decode %XX sequences keeping + literal โ€” for path components (None on invalid UTF-8)
handle_arrow_key(config, key)utils::Keyboard navigation handler
handle_home_end(config, key)utils::Home/End key handler for navigation

(back to top)


CSS Integration

Bootstrap 5

Works out of the box โ€” nav-link and active are native Bootstrap classes.

<ul class="nav nav-pills">
    <li class="nav-item">
        <NavLink<Route> to={Route::Home}>{ "Home" }</NavLink<Route>>
        <!-- Renders: <a class="nav-link active" href="/">Home</a> -->
    </li>
</ul>

Tailwind CSS

Define your own nav-link and active styles:

.nav-link {
    @apply px-4 py-2 text-gray-600 hover:text-gray-900 transition-colors;
}
.nav-link.active {
    @apply text-blue-600 font-semibold border-b-2 border-blue-600;
}

(back to top)


Architecture

Layered module architecture: lib.rs on top; hooks and components in the middle; active_link and nav render primitives below; utils and errors as the Yew-free leaf. Each layer depends only on the layers beneath it.
yew-nav-link
โ”œโ”€โ”€ active_link       # Core NavLink component + Match enum
โ”œโ”€โ”€ nav               # Primitives: NavList, NavItem, NavDivider
โ”œโ”€โ”€ components        # UI: Badge, Dropdown, Icon, Tabs, Pagination
โ”œโ”€โ”€ hooks             # Reactive and programmatic route/navigation helpers
โ”œโ”€โ”€ utils             # Path, URL, keyboard navigation utilities
โ””โ”€โ”€ errors            # NavError, NavResult types

For why the crate is shaped this way โ€” the trade-offs picked over each alternative โ€” see docs/ARCHITECTURE.md for the overview and docs/adr/ for individual architecture decision records.

(back to top)


Examples

A live demo is published at https://raprogramm.github.io/yew-nav-link/. It exercises every component, hook, and utility in the public API.

Run the same demo locally:

rustup target add wasm32-unknown-unknown
cargo install trunk

cd example
trunk serve

Open http://127.0.0.1:3000 (port set in example/trunk.toml).

(back to top)


API Reference

PropTypeDefaultDescription
toR: RoutablerequiredTarget route
childrenChildrenrequiredLink content
partialboolfalseEnable prefix matching (a root route matches only the root path)
classAttrValue"nav-link"Custom CSS class (replaces default)
active_classAttrValue"active"Custom active state class

Match

VariantBehavior
ExactActive only on exact path match
PartialActive when current path starts with target (segment-wise)
fn nav_link<R: Routable + PartialEq + Clone + 'static>(
    to: R,
    children: &str,
    match_mode: Match,
) -> Html
pub struct BreadcrumbItem<R> {
    /// The route this breadcrumb points to.
    pub route: R,
    /// Human-readable label for the breadcrumb.
    pub label: String,
    /// Whether this breadcrumb is the current route.
    pub is_active: bool,
}

(back to top)


Project documentation

FilePurpose
Architectural bookRendered mdBook of the documents below โ€” search, syntax-highlighting, navigation. Source lives under docs/.
docs/REQUIREMENTS.mdFunctional and non-functional requirements (what the crate does, the constraints under which it does it)
docs/ARCHITECTURE.mdModule layout, the active-state algorithm, hook contracts, breadcrumb context flow
docs/ROADMAP.mdTrajectory through 0.10.x (current line) toward 1.0 (API freeze)
docs/BRANCHING.mdBranching, commit, and merge policy enforced on main
SECURITY.mdCoordinated disclosure policy
CONTRIBUTING.mdWorkflow, commit format, code standards

(back to top)


Migration Guides

For a full release-by-release log, see CHANGELOG.md.

FromToNotes
0.8.x0.9.xMacro feature removed; component/function APIs unchanged. See CHANGELOG [0.9.0].
0.9.00.9.1Single-file SPA demo replaces the multi-page docs site under example/; library API unchanged.
0.9.10.9.2BreadcrumbLabelProvider now re-exported at the crate root. Drop-in upgrade.
0.10.x0.11.0use_navigation now routes push/replace/go/go_back/go_forward through the router's Navigator, so a configured basename is honored (previously ignored). Navigation<R>'s internal _marker field is no longer public โ€” construct the handle via use_navigation::<R>(), not a struct literal. The go_back/go_forward fields and all *_callback methods keep the same signatures.

Coverage

Target: 95%+ coverage, tracked via Codecov.

Sunburst

The inner-most circle is the entire project, moving outward are folders then individual files. Size and color represent statement count and coverage.

Sunburst

Grid

Each block represents a single file. Size and color represent statement count and coverage.

Grid

Icicle

Top section is the entire project, proceeding through folders to individual files.

Icicle

(back to top)


Contributing

See CONTRIBUTING.md for the contribution workflow and code standards.

(back to top)


License

Licensed under the MIT License.

(back to top)