La mise à disposition d'un fil d'Ariane (ou breadcrumb) sur un site s'avère utile à plusieurs titres.
Côté SEO bien entendu, avec l'adjonction des données structurées, mais aussi pour faciliter la navigation des utilisateurs. On trouve tout un tas d'articles sur le sujet, assez facilement.
Mais la plupart du temps, ce fil d'Ariane disparait en version mobile car il « prend trop de place » dans les pages.
Le petit souci est qu'en le faisant disparaitre en navigation mobile et sans implémenter d'alternative comme des données JSON, on peut vite perdre des places dans les moteurs de recherche. Mais on peut aussi perdre en route des utilisateurs qui ne savent plus trop bien où ils se trouvent sur le site.
Pour avoir placé des détecteurs de clics sur les fils d'Ariane de ce site, il s'avère même qu'il est autant utilisé en mobile que sur des versions plus larges d'écrans. C'est en effet un moyen très pratique de remonter au niveau supérieur d'une page.
Sauf qu'en mobile, on n'a pas trop de place pour caler tout ça et, en plus, il faut le rendre lisible par les robots, notamment Google qui fouine les pages en mode mobile par défaut dorénavant.
Voici donc une des solutions possibles pour satisfaire à peu près tout le monde.
Articles sur l'importance du fil d'Ariane
- The importance of breadcrumbs, even on mobile
- 6 Important Aspects of Well-Performing Mobile Product Page Breadcrumbs
- Breadcrumbs: we’re doing them wrong
Code HTML
La partie code utilise les attributs nécessaires pour les données structurées mais également un petit peu d'accessibilité, c'est utile. Pour cette partie, il n'y a que deux éléments à ajouter, un aria-label unique et explicite sur la balise nav, et un aria-current="page" sur le dernier élément.
Le reste est du pur HTML standard.
<nav aria-label="Fil d'Ariane" class="breadcrumb">
<ol itemtype="https://schema.org/BreadcrumbList">
<li itemprop="itemListElement" itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/">
<span itemprop="name"><span class="screen-readers">Accueil </span>Ma boutique</span>
</a>
<meta itemprop="position" content="1">
</li>
<li itemprop="itemListElement" itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/rayon/licornes">
<span itemprop="name">Les licornes</span>
</a>
<meta itemprop="position" content="2">
</li>
<li itemprop="itemListElement" itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/rayon/licornes/licorne-magique.html" aria-current="page">
<span itemprop="name">Licorne magique</span>
</a>
<meta itemprop="position" content="3">
</li>
</ol>
</nav>
Cet extrait de code montre un breadcrumb à 3 niveaux, mais ce n'est pas limitatif. La technique fonctionne à l'infini.
Feuille de styles
La feuille de styles est assez simple et « générique » dans le sens où les éléments sont ciblés par rapport à la classe .breadcrumb.
L'adaptation éventuelle sera assez facile.
/*
variable css utilisée en mask : à remplacer selon le site…
--svg-hummingbird: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M8.89,1c-.59,1.97-1.06,3.98-.82,5.91.27,2.11,1.35,4.19,4.24,6.17-3.46,4.83-5.56,13.19-3.83,17.92.7-3.48.92-6.31,3.45-9.33,5-1.65,7.99-2.43,9.05-8.11.4-.95.81-1.25,2.06-1.59.68-.19,1.43-.48,1.43-1.24-.01-3.18-1.83-4.41-3.72-4.38-1.9.03-3.87,1.33-4.22,3.21-.39.12-.77.29-1.14.52-4.06-2.82-5.3-5.54-6.5-9.08h0ZM2,5.64c.73,1.74,1.77,3.49,3.27,4.85,1.59,1.45,3.69,2.47,6.56,2.6-2.25-1.52-3.6-3.18-4.3-4.9-1.91-.67-3.67-1.54-5.52-2.55h0ZM21.3,8.08c.43,0,.79.35.79.79,0,.43-.35.79-.79.79-.43,0-.79-.35-.79-.79,0-.43.35-.79.79-.79ZM24.59,11.19c-.24.32-.49.64-1.07.81,2.12,1.72,4.89,5.15,6.42,7.05-.63-2.17-3.36-5.86-5.34-7.86h0Z"/></svg>');
--svg-marker-arrow-right: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m29.72,15.31L16.88,2.28c-.37-.38-.98-.38-1.35,0l-1.16,1.17c-.37.38-.37.99,0,1.37l7.6,7.71c.6.61.18,1.66-.68,1.66H2.96c-.53,0-.96.43-.96.97v1.66c0,.54.43.97.96.97h18.33c.85,0,1.28,1.05.68,1.66l-7.6,7.71c-.37.38-.37.99,0,1.37l1.16,1.17c.37.38.98.38,1.35,0l12.85-13.03c.37-.38.37-.99,0-1.37Z"/></svg>');
*/
/* Marges contextuelles */
header ~ .breadcrumb:not(.breadcrumb-bottom) {
margin-top: max(calc(var(--header-outer-height) / 2), 2rem);
margin-bottom: var(--small-gutter);
}
main ~ .breadcrumb {
margin-top: var(--gutter, 2rem);
}
/* Base */
.breadcrumb {
font-size: 0.9375rem;
line-height: inherit;
position: relative;
/* Liste */
& *:is(menu, ol, ul) {
line-height: inherit;
font-size: inherit;
white-space: nowrap;
display: flex;
flex-direction: row;
justify-content: flex-start;
max-width: 100%;
overflow: hidden;
}
& li {
display: inline-flex;
font-size: inherit;
/* Icône accueil */
&:first-child a {
padding-left: 2rem;
position: relative;
&::before {
-webkit-mask-repeat: no-repeat;
-webkit-mask-image: var(--svg-hummingbird);
-webkit-mask-size: contain;
-webkit-mask-position: 0.0625rem center;
mask-repeat: no-repeat;
mask-image: var(--svg-hummingbird);
mask-size: contain;
mask-position: 0.0625rem center;
position: absolute;
left: 0;
top: 0;
width: 2rem;
content: "\A0" / "";
background-color: var(--link-color, black);
}
:root.use-keyboard &:focus,
:root:not(.use-keyboard) &:hover {
&::before {
background-color: var(--basic-color-reverse, white);
-webkit-mask-size: 85% 85%;
mask-size: 85% 85%;
}
}
}
/* Séparateurs */
&:not(:first-child)::before {
background-color: CanvasText;
content: "";
min-width: 1.5rem;
display: inline-block;
-webkit-mask-image: var(--svg-marker-arrow-right);
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: 0.75rem;
mask-image: var(--svg-marker-arrow-right);
mask-repeat: no-repeat;
mask-position: center;
mask-size: 0.75rem;
@media (prefers-reduced-motion: no-preference) {
:root:not(.no-motion) & {
transition: transform .2s ease;
}
}
}
/* Animation flèches inversées */
&:is(:focus-within, :hover) ~ li::before {
transform: scaleX(-1);
background-color: var(--link-color);
}
}
/* Liens */
& a {
font-size: inherit;
padding-inline: 0.1875rem;
display: inline-flex;
/* Focus clavier */
:root.use-keyboard &:focus {
outline: none;
background-color: var(--basic-color, black);
border-radius: 3px;
color: var(--basic-color-reverse, white) !important;
& span {
color: inherit;
}
}
/* Hover souris */
@media (any-hover: hover) {
:root:not(.use-keyboard) &:hover {
outline: none;
background-color: var(--basic-color, black);
border-radius: 3px;
color: var(--basic-color-reverse, white) !important;
text-decoration: none !important;
& span {
color: inherit;
}
}
}
/* Page courante */
&[aria-current] > span {
color: var(--basic-color, blue);
overflow-x: hidden;
text-overflow: ellipsis;
font-weight: 500;
max-width: 50ch;
@media (min-width: 64rem) {
max-width: 60ch;
}
}
}
/* Spans visibles */
& span:not(:is(.screen-readers, .sr-only)) {
display: inline-block;
}
/* Overflow horizontal */
&.breadcrumb-overflow *:is(menu, ol, ul) {
overflow-x: auto;
scroll-snap-type: x proximity;
-moz-backface-visibility: hidden;
backface-visibility: hidden;
-ms-overflow-style: none;
scrollbar-width: none;
scroll-behavior: auto;
}
/* Gradient fade-out */
&.overflowed::before {
content: "";
display: block;
pointer-events: none;
position: absolute;
bottom: 0;
top: 0;
width: 7ch;
left: 0;
font-size: inherit;
line-height: inherit;
z-index: 1;
background-color: var(--canvas, white);
-webkit-mask-image: linear-gradient(90deg, #000, rgba(0, 0, 0, 0) 50%);
mask-image: linear-gradient(90deg, #000, rgba(0, 0, 0, 0) 50%);
}
}
/*
Gestion d'affichage du fil d'Ariane de bas de page si présent.
La configuration des templates renvoie la présence d'un breadcrumb.
Un script au début du body gère l'affichage ou non…
*/
/* Masqué par défaut : retiré de l'arbre d'accessibilité et de l'ordre de tabulation */
nav.breadcrumb.breadcrumb-bottom {
display: none;
}
/* La classe posée sur <html> par ton ResizeObserver l'affiche */
:root.display-breadcrumb-bottom nav.breadcrumb.breadcrumb-bottom {
display: block;
}
Javascript
Cette partie est à insérer le plus tôt possible dans le code la page afin de limiter les effets sur le CLS des Core Web Vitals.
(() => {
'use strict';
/**
* Calcule la largeur totale des éléments <li> et stocke leur position horizontale.
* @param {HTMLElement} menu - Le conteneur UL du breadcrumb.
* @returns {number} - La largeur totale cumulée des éléments.
*/
function prepareBreadcrumbItems(menu) {
let totalBreadth = 0;
const items = menu.querySelectorAll('li');
items.forEach(li => {
const offset = li.offsetLeft;
const width = li.offsetWidth;
totalBreadth += width;
li.dataset.offset = `${offset}`;
});
return totalBreadth;
}
/**
* Active le scroll automatique au focus et la gestion de classe CSS selon le scroll.
* @param {HTMLElement} menu - Le conteneur UL du breadcrumb.
* @param {HTMLElement} parent - Le conteneur NAV du breadcrumb.
*/
function enableBreadcrumbScroll(menu, parent) {
const items = menu.querySelectorAll('li');
items.forEach(li => {
li.addEventListener('focusin', () => {
const offset = parseInt(li.dataset.offset, 10);
menu.scrollLeft = offset;
});
});
menu.addEventListener('scroll', () => {
parent.classList.toggle('overflowed', menu.scrollLeft !== 0);
}, {passive: true});
}
/**
* Traitement d'un fil d’Ariane mesurable (affiché).
* @param {HTMLElement} breadcrumb - Le conteneur NAV du breadcrumb.
*/
function initBreadcrumb(breadcrumb) {
const navWidth = breadcrumb.offsetWidth;
const menu = breadcrumb.firstElementChild;
if (!menu || !menu.querySelectorAll) return;
const totalBreadth = prepareBreadcrumbItems(menu);
if (navWidth < totalBreadth) {
const parent = menu.parentElement;
// Scroll vers la fin du fil d’Ariane après le rendu
requestAnimationFrame(() => {
menu.scrollLeft = totalBreadth - navWidth;
});
parent.classList.add('breadcrumb-overflow');
enableBreadcrumbScroll(menu, parent);
}
}
/**
* Initialisation principale : traitement de chaque fil d’Ariane.
*/
const breadcrumbs = [...document.querySelectorAll('nav.breadcrumb')];
if (breadcrumbs.length === 0) return;
breadcrumbs.forEach(breadcrumb => {
if (breadcrumb.offsetWidth) return initBreadcrumb(breadcrumb);
// Nav en display: none (offsetWidth et offsetLeft valent 0) :
// mesure différée à sa première apparition, une seule fois
const ro = new ResizeObserver(() => {
if (!breadcrumb.offsetWidth) return;
ro.disconnect();
initBreadcrumb(breadcrumb);
});
ro.observe(breadcrumb);
});
})();
Application
Une fois le script exécuté, il va faire plusieurs choses.
- Vérifier la présence d'au moins un fil d'Ariane ;
- Vérifier s'il y a assez de place dans sa zone d'affichage. Si oui, il ne fait rien ;
- Si le fil d'Ariane manque de place, il est transformé en zone défilante et prend en charge la navigation clavier.
Dans tous les cas, le fil ne revient jamais à la ligne.
Au chargement des pages, le dernier item de la liste est mis en avant.
Comment tester ? Regardez les pages de ce site en mobile, ça peut donner une idée.
Et c'est tout…
That's all Folks.