Pourquoi s'intéresser à la Navigation API ?
Pendant longtemps, gérer la navigation côté navigateur voulait dire choisir entre deux extrêmes : laisser le navigateur recharger entièrement la page à chaque lien, ou construire une Single Page Application avec un routeur JavaScript complet. Entre les deux, l'History API a permis de manipuler pushState, replaceState et popstate, mais elle reste assez basse niveau.
La Navigation API propose une approche plus moderne : intercepter certaines navigations, charger du contenu à la demande, gérer l'historique, les erreurs et les transitions avec une API conçue explicitement pour ces usages.
Elle ne remplace pas forcément un routeur de framework. Elle est plutôt intéressante pour des sites hybrides, des applications légères, des interfaces en progressive enhancement, ou des composants de navigation avancés. Si vous avez déjà travaillé avec la View Transitions API, la Speculation Rules API ou le progressive enhancement, la Navigation API s'inscrit dans la même logique : enrichir l'expérience sans casser les bases du web.
Le problème de l'History API
L'History API permet de modifier l'URL sans recharger la page :
history.pushState({ page: 'products' }, '', '/products')
window.addEventListener('popstate', event => {
console.log('Navigation historique', event.state)
})
Cela fonctionne, mais plusieurs responsabilités restent à votre charge :
- intercepter les clics sur les liens ;
- distinguer les liens internes et externes ;
- gérer les retours arrière et avant ;
- annuler des chargements concurrents ;
- afficher les erreurs ;
- maintenir un état cohérent entre l'URL, le DOM et les données.
Dans une petite interface, ce bricolage peut sembler acceptable. Dans une application plus riche, il devient vite fragile. C'est précisément le type de problème que la Navigation API cherche à clarifier.
Détecter le support de l'API
Comme pour beaucoup d'API web modernes, il faut commencer par un test de support. Le but n'est pas de rendre votre site dépendant de JavaScript, mais d'améliorer l'expérience lorsque l'API est disponible.
if ('navigation' in window) {
console.log('Navigation API disponible')
} else {
console.log('Fallback vers une navigation classique')
}
Cette approche est importante : un lien HTML doit rester un lien HTML. Si JavaScript échoue, si l'API n'est pas disponible, ou si l'utilisateur ouvre le lien dans un nouvel onglet, la navigation native doit continuer à fonctionner.
C'est le même principe que pour la Popover API ou le Declarative Shadow DOM : partir d'une base HTML robuste, puis ajouter une couche d'amélioration.
Intercepter une navigation interne
Le coeur de l'API repose sur l'événement navigate. Il permet d'observer une navigation et, dans certains cas, de l'intercepter.
if ('navigation' in window) {
navigation.addEventListener('navigate', event => {
const url = new URL(event.destination.url)
if (url.origin !== location.origin) {
return
}
if (!event.canIntercept) {
return
}
event.intercept({
async handler() {
await renderPage(url)
}
})
})
}
Dans cet exemple, on ne traite que les URL de la même origine. Les liens externes restent gérés normalement par le navigateur. On vérifie aussi event.canIntercept, car toutes les navigations ne sont pas interceptables.
La fonction renderPage peut ensuite charger des données, mettre à jour le DOM, gérer un état de chargement ou afficher une erreur.
async function renderPage(url: URL) {
const outlet = document.querySelector<HTMLElement>('[data-page-outlet]')
if (!outlet) {
return
}
outlet.textContent = 'Chargement...'
const response = await fetch(`/partials${url.pathname}`)
if (!response.ok) {
outlet.textContent = 'Impossible de charger cette page.'
return
}
outlet.innerHTML = await response.text()
}
Ce code reste volontairement simple. En production, il faudrait éviter d'injecter du HTML non contrôlé avec innerHTML, surtout si le contenu peut inclure des données utilisateur. Sur ce sujet, vous pouvez relire l'article sur Trusted Types et celui sur la Content Security Policy.
Gérer l'annulation des chargements
Une difficulté fréquente dans les interfaces de navigation est la concurrence entre plusieurs actions. L'utilisateur clique sur une page, puis très vite sur une autre. Le premier chargement peut terminer après le second et écraser le contenu avec une page périmée.
La Navigation API s'intègre bien avec les signaux d'annulation. L'événement fournit un signal utilisable avec fetch.
navigation.addEventListener('navigate', event => {
const url = new URL(event.destination.url)
if (url.origin !== location.origin || !event.canIntercept) {
return
}
event.intercept({
async handler() {
const response = await fetch(`/api/page?path=${url.pathname}`, {
signal: event.signal
})
const data = await response.json()
updateView(data)
}
})
})
Ce modèle évite une partie des effets de bord classiques des interfaces asynchrones. Il reste cependant utile de structurer proprement vos appels HTTP, comme dans l'article sur AbortController en JavaScript, surtout si plusieurs couches de votre application déclenchent des requêtes.
Ajouter une transition visuelle
Une navigation fluide ne consiste pas seulement à charger vite. Il faut aussi préserver la compréhension de l'utilisateur : où suis-je allé, qu'est-ce qui a changé, puis-je revenir en arrière ?
La Navigation API peut être combinée avec document.startViewTransition lorsque l'API est disponible.
async function transitionTo(url: URL) {
if (!document.startViewTransition) {
await renderPage(url)
return
}
const transition = document.startViewTransition(async () => {
await renderPage(url)
})
await transition.finished
}
Puis dans l'interception :
navigation.addEventListener('navigate', event => {
const url = new URL(event.destination.url)
if (url.origin !== location.origin || !event.canIntercept) {
return
}
event.intercept({
handler() {
return transitionTo(url)
}
})
})
Cette combinaison permet de créer des transitions de page sans importer une bibliothèque lourde. Elle doit toutefois rester sobre. Une transition trop longue ou trop systématique peut nuire à l'expérience, en particulier pour les utilisateurs sensibles au mouvement. Pensez à respecter prefers-reduced-motion côté CSS.
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.001s;
}
}
Conserver une architecture claire
Le piège serait de transformer un petit intercepteur de navigation en routeur maison mal défini. Pour éviter cela, séparez les responsabilités.
Une première fonction peut décider si une navigation doit être interceptée :
function shouldHandleNavigation(event: NavigateEvent): boolean {
const url = new URL(event.destination.url)
return event.canIntercept &&
url.origin === location.origin &&
!url.pathname.startsWith('/admin')
}
Une deuxième peut charger les données :
type PageData = {
title: string
content: string
}
async function loadPage(url: URL, signal: AbortSignal): Promise<PageData> {
const response = await fetch(`/api/pages${url.pathname}`, { signal })
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`)
}
return response.json()
}
Une troisième peut mettre à jour l'interface :
function renderPageData(data: PageData) {
document.title = data.title
const outlet = document.querySelector('[data-page-outlet]')
if (outlet) {
outlet.textContent = data.content
}
}
Cette séparation facilite les tests, la maintenance et l'évolution progressive. Si votre logique d'état devient plus complexe, un modèle explicite comme les machines à états en TypeScript peut aussi aider.
Et avec React, Vue, Nuxt ou Next ?
Dans une application React, Vue, Nuxt ou Next, vous utilisez généralement déjà un routeur. La Navigation API n'a donc pas vocation à remplacer directement React Router, Vue Router ou les routeurs intégrés aux frameworks fullstack.
Elle devient pertinente dans d'autres contextes :
- un site principalement rendu serveur qui veut améliorer certaines navigations ;
- une documentation avec chargement partiel de contenu ;
- une application sans framework lourd ;
- un composant embarqué dans une page existante ;
- une expérimentation autour des transitions natives.
Dans un projet Next ou Nuxt, il faut éviter de court-circuiter le routeur du framework sans raison. En revanche, comprendre la Navigation API aide à mieux raisonner sur les abstractions fournies par ces outils.
Bonnes pratiques à retenir
Ne partez pas de JavaScript. Commencez par des liens HTML classiques :
<nav aria-label="Navigation principale">
<a href="/produits">Produits</a>
<a href="/tarifs">Tarifs</a>
<a href="/contact">Contact</a>
</nav>
<main data-page-outlet>
<!-- Contenu initial rendu par le serveur -->
</main>
Ensuite seulement, interceptez les navigations compatibles. Gardez aussi en tête ces règles :
- ne bloquez pas les liens externes ;
- laissez les ouvertures en nouvel onglet fonctionner ;
- gérez les erreurs réseau ;
- annulez les chargements obsolètes ;
- mettez à jour le titre de la page ;
- préservez le focus clavier après navigation ;
- mesurez l'impact réel avec des outils comme PerformanceObserver.
L'accessibilité est particulièrement importante. Après une navigation interceptée, le lecteur d'écran ne perçoit pas forcément un changement de page complet. Vous pouvez déplacer le focus vers le titre principal ou annoncer le changement via une région dédiée, comme expliqué dans l'article sur les ARIA live regions.
Conclusion
La Navigation API apporte une brique intéressante au développement frontend moderne. Elle permet de gérer des navigations enrichies sans abandonner les mécanismes fondamentaux du web : liens, historique, URLs et rendu serveur.
Son intérêt principal n'est pas de remplacer tous les routeurs JavaScript. Il est plutôt de fournir un niveau d'abstraction natif pour les sites et applications qui veulent plus de fluidité, moins de code fragile autour de l'History API, et une meilleure intégration avec les API modernes du navigateur.
Comme souvent, la bonne stratégie consiste à l'adopter progressivement : HTML fonctionnel d'abord, interception ensuite, transitions et optimisations enfin.