Navigate Without Reloads Using Framework7’s Router
Framework7’s client-side Router swaps pages via Ajax and pushState. Learn lifecycle scoping, route params, and deep-link limits.
26 Feb 2026, 01:57 UTC

You tap a link in a Framework7 mobile web app and the whole screen flickers as the browser reloads every stylesheet and script. Shared headers re-parse, scroll position resets, and the back button stops behaving like an app. The useful takeaway is that Framework7's built-in Router can load page HTML with Ajax, update the URL using pushState, and keep the app shell intact—so navigation feels native. The catch is that you must initialize page-specific JavaScript through lifecycle events, and your server must be configured to serve the app shell on refresh for deep links.
How the Router handles navigation
Framework7's Router manages one or more Views. When a user follows a route, the Router fetches the next page's HTML with Ajax, swaps it into the active View, and updates the browser URL with pushState. The full document is not reloaded, so shared CSS and JavaScript stay parsed and the transition is faster on slow mobile connections.
This only works for same-origin resources, or external pages served with CORS. Browsers block Ajax-loaded HTML from a different origin unless the server explicitly allows it.
Scope initialization to page lifecycle events
Because pages enter and leave the DOM as the user navigates, global DOMContentLoaded handlers are not enough. Framework7 fires page-scoped events such as:
page:init— run setup when a page is inserted into the DOM.page:beforein— run logic just before the page becomes visible.page:afterout— run cleanup after the page leaves the DOM.
Attach listeners inside page:init using the page element as context, and remove them—or use event delegation—when the page exits. Listeners left on detached elements are a common source of memory leaks in long-running mobile sessions.
Some Framework7 components do not auto-initialize on Ajax-loaded pages. Pass the page element to the component's initialization method so it binds to the correct DOM subtree.
Declare routes with path, URL, and parameters
Routes are declared with path, url, and optional async handlers. A parameterized route such as /product/:id lets the Router extract the ID from the URL and pass it to the page handler. Lazy-loaded page URLs reduce the initial bundle because content is fetched only when the user navigates to it.
Worked example: two routes, lifecycle logging, and verification
Where to run: create these files in your project directory and open index.html in a browser. Permissions: standard file write access; no server is required for local file testing, though Ajax page loading may fail under strict file:// restrictions depending on the browser. Placeholders: replace APP_ROOT with your project path.
Expected checks:
- Clicking a link updates the address bar without a full reload.
- The Network tab shows page HTML fetched asynchronously while shared assets are not re-downloaded.
- Console logs appear only when the corresponding page enters or exits the DOM.
Risks: Do not attach global listeners inside page:init without removal logic. Refreshing a nested route may return 404 unless your server is configured to return the app shell for all client-side routes.
<!-- index.html -->
<link rel='stylesheet' href='framework7.css'>
<div id='app'>
<div class='view view-main'>
<div class='page' data-name='home'>
<div class='page-content'>
<a href='/about'>About</a>
</div>
</div>
</div>
</div>
<script src='framework7.js'></script>
<script>
const app = new Framework7({
root: '#app',
routes: [
{ path: '/', url: './pages/home.html' },
{ path: '/about', url: './pages/about.html' }
]
});
document.addEventListener('page:init', function (event) {
const page = event.detail && event.detail.page ? event.detail.page : event.target;
if (page && page.name === 'about') {
console.log('About page initialized');
// Attach page-scoped listeners here. Adjust the page reference
// to match your Framework7 version's event object.
}
});
document.addEventListener('page:afterout', function (event) {
const page = event.detail && event.detail.page ? event.detail.page : event.target;
if (page && page.name === 'about') {
console.log('About page removed from DOM');
// Remove listeners or clean up timers here.
}
});
</script>
To verify deep-link behavior, refresh the browser while on /about. If the server returns a 404 instead of index.html, configure a fallback rule so every client-side route serves the Framework7 shell. This is not a Framework7 setting; it is a server configuration task.
Trade-offs and limits
| Behavior | Benefit | Limitation |
|---|---|---|
| Ajax page loading | Shared assets stay parsed; faster perceived navigation | Same-origin or CORS required |
pushState URL history | Back button works without full reloads | Deep links 404 on refresh without server fallback |
| Page lifecycle events | Scoped setup and cleanup per page | Manual listener removal needed to avoid leaks |
| Lazy-loaded routes | Smaller initial startup payload | Navigation delay while page HTML fetches |
Closing checklist
- Declare routes with
pathandurlbefore initializing the app. - Bind page-specific JavaScript to
page:initand clean up inpage:afterout. - Open the Network tab during navigation and confirm page HTML loads asynchronously.
- Refresh on a nested route and confirm the server returns the app shell, not a 404.
- Use the preloader or progress bar components during Ajax transitions if page fetch takes more than a fraction of a second.
Framework7's Router removes full-page reloads from mobile navigation, but it shifts responsibility for lifecycle scoping and server fallback to you. Verify the two behaviors above before treating client-side routing as complete.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.