Declarative client-side routing for ShadowJS applications with support for nested routes, layouts, transitions, and scroll restoration.
- π― Declarative Routing: Define routes using JSX components
- π§© Nested Routes: Support for nested layouts and route hierarchies
- π Transitions: Built-in route transition animations
- π Scroll Restoration: Automatic scroll position management
- π Programmatic Navigation: Navigate programmatically with full control
- π¨ Layout Support: Shared layouts for multiple routes
- π± Hash & History Modes: Choose between hash and history-based routing
- π Route Parameters: Dynamic route segments with type safety
The ShadowJS Router requires @shadow-js/core as a peer dependency:
# Install both packages together
npm install @shadow-js/router @shadow-js/core
# Or if you already have @shadow-js/core installed
npm install @shadow-js/routerImportant: This router is specifically designed for ShadowJS applications and requires @shadow-js/core to function properly. The router imports directly from @shadow-js/core and will not work without it.
For development and testing, you can use the development configuration:
# Copy the development package.json
cp package.json.dev package.json
# Install dependencies (includes @shadow-js/core)
npm installThis package uses a peer dependency approach for several reasons:
- Framework Integration: Tightly coupled with ShadowJS reactive system
- Bundle Size: Avoids bundling framework code that consumers already have
- Version Alignment: Ensures consumers use compatible versions
- Tree Shaking: Allows better optimization in consumer applications
import { Router, Route, A, useLocation } from "@shadow-js/router";
function Navigation() {
return (
<nav>
<A href="/">Home</A>
<A href="/about">About</A>
<A href="/contact">Contact</A>
</nav>
);
}
function Home() {
const location = useLocation();
return (
<div>
<h1>Home</h1>
<p>Current path: {() => location.pathname}</p>
</div>
);
}
function About() {
return <h1>About Us</h1>;
}
function Contact() {
return <h1>Contact Us</h1>;
}
function NotFound() {
return <h1>404 - Page Not Found</h1>;
}
function App() {
return (
<Router notFound={NotFound}>
<Navigation />
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Router>
);
}import { useParams } from "@shadow-js/router";
function UserProfile() {
const params = useParams();
return (
<div>
<h1>User Profile</h1>
<p>User ID: {() => params().id}</p>
</div>
);
}
function App() {
return (
<Router>
<Route path="/users/:id" component={UserProfile} />
</Router>
);
}Routes can be configured in two ways:
<Router>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/users/:id" component={UserProfile} />
</Router>const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
{ path: "/users/:id", component: UserProfile },
];
<Router routes={routes} />;ShadowJS Router uses a sophisticated matching algorithm:
- Exact Matching:
/usersmatches only/users - Parameter Matching:
/users/:idmatches/users/123 - Wildcard Matching:
/users/*matches/users/123/posts - Optional Parameters:
/users/:id?matches/usersand/users/123
function DashboardLayout({ children }) {
return (
<div className="dashboard">
<nav>
<A href="/dashboard">Overview</A>
<A href="/dashboard/users">Users</A>
<A href="/dashboard/settings">Settings</A>
</nav>
<main>{children}</main>
</div>
);
}
function DashboardOverview() {
return <h2>Dashboard Overview</h2>;
}
function App() {
return (
<Router>
<Route path="/dashboard" layout={DashboardLayout}>
<Route path="/" component={DashboardOverview} />
<Route path="/users" component={DashboardUsers} />
<Route path="/settings" component={DashboardSettings} />
</Route>
</Router>
);
}The main router component that manages client-side routing for your application.
Props:
routes?: RouteConfig[]- Array of route configurations (alternative to children)children?: any- Child routes when using JSX syntaxtransition?: Transition- Default transition for all routesnotFound?: ComponentType- Component to render for unmatched routesmode?: "history" | "hash"- URL handling mode (default: "history")scroll?: ScrollBehavior- Scroll restoration behavior
Examples:
// JSX syntax
<Router notFound={NotFound} mode="history">
<Route path="/" component={Home} />
<Route path="/about" component={About} />
</Router>;
// Object syntax
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
];
<Router routes={routes} />;Defines a route with its path and component.
Props:
path: string- Route path patterncomponent?: ComponentType- Component to render for this routelayout?: ComponentType- Layout component wrapping this routechildren?: RouteConfig[]- Nested routesredirect?: string | Function- Redirect path or functiontransition?: Transition- Route-specific transition
Examples:
// Basic route
<Route path="/home" component={Home} />
// Route with layout
<Route path="/dashboard" layout={DashboardLayout}>
<Route path="/" component={Overview} />
<Route path="/users" component={Users} />
</Route>
// Route with redirect
<Route path="/old-path" redirect="/new-path" />
// Dynamic route
<Route path="/users/:id" component={UserProfile} />Enhanced anchor tag for client-side navigation with automatic active states.
Props:
href: string- Navigation targetreplace?: boolean- Replace current history entrystate?: any- State to pass to the new routeclassName?: string | Function- CSS class (can be reactive)- All standard
<a>tag props
Examples:
<A href="/home">Home</A>
<A href="/users" className={() => (location.pathname === "/users" ? "active" : "")}>
Users
</A>Programmatically redirects to another route.
Props:
to: string- Redirect target path
Examples:
<Redirect to="/login" />Returns the current location object with reactive properties.
Returns: Store<Location>
Location Properties:
pathname: string- Current pathsearch: string- Query stringhash: string- Hash fragmentstate: any- Navigation state
Examples:
function CurrentPage() {
const location = useLocation();
return (
<div>
<p>Current path: {location.pathname}</p>
<p>Search: {location.search}</p>
<p>Hash: {location.hash}</p>
</div>
);
}Returns route parameters extracted from the current URL.
Returns: Store<T | undefined>
Examples:
function UserProfile() {
const params = useParams<{ id: string }>();
return <div>User ID: {params().id}</div>;
}Manages URL search parameters with reactive updates.
Returns: [Store<URLSearchParams>, SetterFunction]
Examples:
function SearchComponent() {
const [searchParams, setSearchParams] = useSearchParams();
const updateSearch = (query: string) => {
setSearchParams({ q: query, page: "1" });
};
return (
<input
value={searchParams().get("q") || ""}
onInput={(e) => updateSearch(e.target.value)}
/>
);
}Programmatically navigate to a new route.
Parameters:
to: string- Target pathoptions?: NavigateOptions- Navigation options
NavigateOptions:
replace?: boolean- Replace current history entrystate?: any- State to pass to the new route
Examples:
// Basic navigation
navigate("/dashboard");
// Replace current history entry
navigate("/login", { replace: true });
// Pass state
navigate("/checkout", { state: { fromCart: true } });Redirect to a new route (equivalent to navigate with replace: true).
Parameters:
to: string- Redirect target
Examples:
redirect("/login");Configuration object for defining routes programmatically.
Properties:
path: P- Route path patterncomponent?: ComponentType- Route componentlayout?: ComponentType- Layout componentchildren?: RouteConfig[]- Nested routesredirect?: string | Function- Redirect configurationtransition?: Transition- Route transition
Type-safe route parameters extracted from path patterns.
Examples:
// For path "/users/:id/posts/:postId"
// PathParams = { id: string, postId: string }Options for programmatic navigation.
Properties:
replace?: boolean- Replace history entrystate?: T- Navigation state
Animation effect for route changes.
Types:
"none"- No transition"fade"- Fade transitionCustomTransition- Custom transition function
import { navigate, redirect } from "@shadow-js/router";
function LoginForm() {
const [isLoggedIn, setIsLoggedIn] = useStore(false);
const handleLogin = () => {
setIsLoggedIn(true);
// Navigate after successful login
navigate("/dashboard", { replace: true });
};
const handleLogout = () => {
setIsLoggedIn(false);
// Redirect to home
redirect("/");
};
return (
<div>
<button onClick={handleLogin}>Login</button>
<button onClick={handleLogout}>Logout</button>
</div>
);
}<Router transition="fade">
<Route path="/" component={Home} />
<Route path="/about" component={About} transition="slide" />
</Router>Available transitions: "fade", "slide", "scale", "none"
<Router
scroll={(context) => {
if (context.action === "PUSH") {
window.scrollTo(0, 0); // Scroll to top on navigation
} else if (context.action === "POP") {
// Restore scroll on browser back/forward
// Default behavior handles this automatically
}
}}
>
{/* routes */}
</Router>function ProtectedRoute({ component: Component }) {
const [isAuthenticated, setIsAuthenticated] = useStore(false);
return (
<Show when={() => isAuthenticated()} fallback={<Redirect to="/login" />}>
<Component />
</Show>
);
}
function App() {
return (
<Router>
<Route
path="/dashboard"
component={() => <ProtectedRoute component={Dashboard} />}
/>
</Router>
);
}import { useSearchParams } from "@shadow-js/router";
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = () => searchParams().get("category") || "all";
const sort = () => searchParams().get("sort") || "name";
const updateFilters = (newFilters) => {
setSearchParams({
...Object.fromEntries(searchParams().entries()),
...newFilters,
});
};
return (
<div>
<select
value={() => category()}
onChange={(e) => updateFilters({ category: e.target.value })}
>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
</div>
);
}The router compiles route configurations into an optimized matching tree:
- Parse route patterns into regex and parameter names
- Build matching tree for efficient route resolution
- Cache compiled routes for performance
- URL Change Detection: Listen to
popstateandhashchangeevents - Route Matching: Find matching route for current path
- Component Resolution: Resolve component and layout hierarchy
- Transition Handling: Apply route transitions
- Scroll Management: Handle scroll restoration
- Render: Update DOM with new route content
The router manages several reactive states:
- Current location: pathname, search, hash
- Current route match: matched route and parameters
- Navigation history: for back/forward functionality
- Scroll positions: for restoration
import { useStore } from "@shadow-js/core";
import { Router, Route, useLocation } from "@shadow-js/router";
function App() {
const [user, setUser] = useStore(null);
return (
<Router>
<Route path="/" component={Home} />
<Route path="/profile" component={Profile} />
</Router>
);
}// For server-side rendering or custom history management
const customHistory = {
push: (path) => {
/* custom push logic */
},
replace: (path) => {
/* custom replace logic */
},
go: (delta) => {
/* custom go logic */
},
};
// Pass to router
<Router history={customHistory}>{/* routes */}</Router>;Routes can have their own styling:
// Route-specific styles
<Route path="/dashboard" component={Dashboard} className="dashboard-route" />;
// Conditional styling based on active route
function Navigation() {
const location = useLocation();
return (
<nav>
<A href="/" className={() => (location.pathname === "/" ? "active" : "")}>
Home
</A>
<A
href="/about"
className={() => (location.pathname === "/about" ? "active" : "")}
>
About
</A>
</nav>
);
}function App() {
return (
<Router
notFound={() => <div>Custom 404 Page</div>}
onError={(error) => {
console.error("Route error:", error);
// Handle route errors
}}
>
{/* routes */}
</Router>
);
}import { navigate } from "@shadow-js/router";
try {
await navigate("/protected-route");
} catch (error) {
if (error.code === "UNAUTHORIZED") {
navigate("/login");
}
}// Preload routes on hover for better UX
function Link({ href, children }) {
const [isPreloading, setIsPreloading] = useStore(false);
return (
<A
href={href}
onMouseEnter={() => {
setIsPreloading(true);
// Preload route component
import(`./pages${href}.js`);
}}
className={() => (isPreloading() ? "preloading" : "")}
>
{children}
</A>
);
}Routes automatically memoize components to prevent unnecessary re-renders.
import { render } from "@shadow-js/core";
import { Router, Route } from "@shadow-js/router";
describe("Routing", () => {
test("renders correct component for route", () => {
const container = document.createElement("div");
render(
<Router>
<Route path="/test" component={() => <div>Test Component</div>} />
</Router>,
container
);
// Navigate to test route
window.history.pushState({}, "", "/test");
expect(container.innerHTML).toContain("Test Component");
});
});See the examples documentation for:
- Basic routing patterns
- Nested routes and layouts
- Dynamic routing
- Protected routes
- Search and query parameters
We welcome contributions! See the Contributing Guide for details.
MIT License - see LICENSE for details.
- Documentation: Router API
- Examples: Routing Examples
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Built with β€οΈ for the ShadowJS ecosystem.