Routing a Small SPA with Mithril’s m.route
Use Mithril’s built-in m.route for client-side navigation without a heavy router, plus what you still must build yourself.
28 Feb 2026, 04:18 UTC

The problem: a router shouldn’t outweigh your app
You are building a small single-page application (SPA): a site that rewrites the current page instead of loading a new one from the server. You need the URL to change when the user clicks a link, the browser’s back button to work, and direct links to /about or /contact to open the right view. The obvious choice in many ecosystems is a full-featured routing library, but for a small project that library can add more code than the rest of your application combined.
Mithril includes a client-side router called m.route. It maps URL patterns to Mithril components and handles navigation without a separate dependency. This post shows how to set it up, where it fits, and what you must still build yourself.
How m.route maps URLs to components
m.route takes a DOM element, a default route, and a route table. The route table is an object where keys are URL paths and values are Mithril components. When the URL changes, Mithril renders the matching component inside the root element and diffs only the changed DOM nodes.
Mithril supports two URL styles:
- History mode: clean URLs such as /about. Requires your server to return index.html for unknown paths so a refresh does not produce a 404.
- Hash mode: URLs such as /#/about. Works on static file hosts without server configuration, but less readable.
A component in Mithril is an object with a view function that returns virtual DOM descriptions. Mithril’s virtual DOM diffing updates only the parts of the real DOM that changed.
Worked example: a three-page app with a route guard
Run the following in your project root. You need Node.js and npm; no special permissions are required for a local project.
npm init -y
npm install mithril@^2.0.2
Create index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Mithril route example</title>
</head>
<body>
<div id="app"></div>
<script src="app.js"></script>
</body>
</html>
Create app.js:
import m from "mithril";
const Home = {
view: () => m("section", [
m("h1", "Home"),
m("p", "Welcome to the application."),
m("a", { href: m.route.link("/about") }, "Go to About")
])
};
const About = {
view: () => m("section", [
m("h1", "About"),
m("p", "A small Mithril SPA."),
m("a", { href: m.route.link("/contact") }, "Go to Contact")
])
};
const Contact = {
view: () => m("section", [
m("h1", "Contact"),
m("p", "Email: [contact removed]")
])
};
const Login = {
view: () => m("section", [
m("h1", "Login"),
m("p", "Please sign in to continue."),
m("a", { href: m.route.link("/") }, "Back to home")
])
};
const Dashboard = {
view: () => m("section", [
m("h1", "Dashboard"),
m("p", "Authenticated content."),
m("a", { href: m.route.link("/") }, "Home")
])
};
const isAuthenticated = false; // replace with your auth check
const DashboardRoute = {
oninit: () => {
if (!isAuthenticated) {
m.route.set("/login");
}
},
view: () => isAuthenticated ? m(Dashboard) : null
};
m.route(document.getElementById("app"), "/", {
"/": Home,
"/about": About,
"/contact": Contact,
"/login": Login,
"/dashboard": DashboardRoute
});
Expected behavior when you open index.html in a browser:
- The address bar shows / and the Home component renders inside #app.
- Clicking “Go to About” changes the URL to /about without a full page reload.
- Clicking the dashboard link redirects to /login because isAuthenticated is false.
To verify, open your browser’s developer tools, select the Network tab, and confirm that navigating between routes does not request a new HTML page. Then refresh the browser while on /about. If you are using history mode on a real server, the server must return index.html or you will see a 404.
Trade-offs: what m.route does not provide
Mithril’s router is intentionally minimal. It does not include:
- Built-in authentication or authorization guards. You must check credentials in oninit or a wrapper component, as shown above.
- Server-side rendering. m.route runs in the browser; rendering routes on the server requires a separate setup.
- Nested route outlets or advanced middleware. Deeply nested route tables are possible but can become hard to maintain; keep the route tree shallow.
If you choose history mode, configure your development server to fall back to index.html. For example, route all unknown paths to index.html before serving static files. In production, most static hosts have a single-page app or rewrite setting for this.
Check the result and decide
Open the browser console and run:
m.route.get()
It should return the current path, such as /about. If it returns the expected path and the view updates without a page reload, the router is wired correctly.
Use m.route when your application is small to medium-sized, you control the server configuration or are willing to use hash mode, and you want to avoid a large routing dependency. If you need server-side rendering, complex nested routes, or declarative guards out of the box, look at a more opinionated framework or router.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.