7min.

En finir avec la barre de défilement horizontale et les unités de viewport

J’ai l’habitude de développer sur Mac OS, et sur ses navigateurs (que ce soit Chrome, Firefox ou Safari), la barre de défilement s’affiche par-dessus la page web et est invisible par défaut. C’est joli mais pas très pratique pour un·e développeur·euse. En effet on a vite fait de passer à côté d’une vilaine barre de défilement horizontale.

C’est ce qu’il s’est passé sur un site sur lequel j’ai travaillé récemment. En intégrant le nouveau menu du site, j’ai créé, sans m’en rendre compte, une barre de défilement horizontale sur le site. Elle n’était d’ailleurs pas juste invisible par défaut, mais totalement absente sur Mac OS alors que bien visible sur Windows. La plupart des personnes travaillant sur le projet étant sur Mac OS, on est un peu toutes et tous passé·e·s à côté.

Après m’être auto-flagellée, j’ai donc investigué sur le problème avant de me rendre compte que c’était l’utilisation d’une règle css width: 100vw; qui faisait apparaître cette barre. L’occasion de creuser un peu plus le sujet de ce bug, pas si anodin.

Section intitulée le-probleme-avec-les-unites-de-viewportLe problème avec les unités de viewport

Les unités de viewport sont des unités relatives qui représentent un pourcentage de la taille du viewport. Ainsi 1vh représente 1% de la hauteur du viewport. Bien pratique pour afficher une section de type « hero » par exemple qui prendrait toute la hauteur de l’écran. Cependant un bug bien connu sur mobile a longtemps embêté les intégrateur·ice·s. La zone représentée par 100vh déborde de la zone visible lors de l’affichage de la barre d’adresse ou des éléments d’UI du navigateur.

L’arrivée de nouvelles unités a permis de corriger ce problème. Désormais les unités svh, lvh ou dvh représentent respectivement la petite zone d’affichage (avec la barre d’adresse), la grande zone d’affichage (sans barre d’adresse) ou dynamiquement la petite ou grande zone en fonction de celle qui correspond à la zone visible au-dessus de la ligne de flottaison. Plus de problème donc de contenu qui « déborde ». En appliquant une hauteur de 100dvh (ou 100 svh) à un élément, on est sûrs qu’il ne dépassera jamais de la ligne de flottaison.

Cependant, on a un autre souci avec les unités de viewport. Celui-ci concerne cette fois-ci la largeur d’écran (vw). La taille 100vw représente 100% de la largeur du viewport sauf dans un cas : lorsque le site possède une barre de défilement verticale, 100vw représente alors la largeur du viewport + la largeur de la barre de défilement. Si on définit la largeur d’un élément à 100vw, celui-ci dépassera donc en largeur dans le cas où le site possède une barre de défilement verticale.

On comprend mieux pourquoi le problème n’est pas présent sur Mac OS : la barre s’affichant en superposition de la page, elle ne s’ajoute pas à la valeur de 100vw. Le problème ne survient que si on a une barre classique qui s’affiche dans une gouttière à côté du viewport.

On pourrait penser que les nouvelles unités svw, lvw et dvw nous permettraient de corriger ce problème mais ce n’est malheureusement pas le cas. Il y a eu de nombreux débats sur ce sujet sur le dépôt du CSS Working Group mais la difficulté est qu’il faudrait attendre que la page soit chargée pour savoir si celle-ci possède une barre verticale. Seulement dans ce cas, il faudrait soustraire la largeur de la barre de la largeur du viewport. Cela impliquerait donc de recalculer les styles à la fin du chargement si on a finalement une barre verticale.

Section intitulée les-differentes-solutions-au-problemeLes différentes solutions au problème

Maintenant que l’on sait, et que l’on comprend, pourquoi on obtient une barre horizontale en utilisant 100vw, comment corriger ce problème ? Je vous entends déjà me dire « on n’a qu’à rajouter un overflow-x: hidden des familles et hop c’est réglé » (si si, je vous entends). Mais je vous arrête tout de suite : ici pas de solution cache misère. Certes, cela fonctionnerait mais on va voir comment éviter ce problème plutôt que de le masquer.

La première étape, si vous êtes sur MacOS, c’est d’activer l’affichage des barres de défilement pour obtenir des barres classiques comme sur Windows. Cela vous évitera de passer à côté du problème que tous les utilisateur·rice·s de Windows auront sur votre site. Pour cela, rendez vous dans Réglages Système > Apparence puis sélectionnez « toujours » pour l’option « Afficher les barres de défilement ».

Section intitulée la-solution-evidente-n-utilisez-pas-100vwLa solution évidente : n’utilisez pas 100vw

Ça parait évident vu comme ça, mais effectivement on a finalement rarement besoin d’utiliser la valeur 100vw. Il suffit parfois de remplacer l’utilisation des unités de viewport par les pourcentages. Essayez simplement de remplacer 100vw par 100%. Il y a cependant un cas où cela ne fonctionnera pas. Si vous avez un élément enfant inclus dans un parent qui a une largeur maximale. À moins que cet enfant soit positionné en absolute (et que le parent soit en position static), lui donner une largeur de 100% lui fera prendre la largeur de son parent et non la largeur du viewport.

Une solution, que vous avez peut-être déjà utilisée, consiste à appliquer une marge négative à droite et à gauche de l’élément.

.container {
  max-width: 1024px;
  margin-left: auto;
  margin-right: auto;
}

.full-element {
  margin-left: calc(-50vw + 50%);
  margin-right: calc(-50vw + 50%);
}

Cependant cette solution repose aussi sur les viewport width et causera également l’apparition d’une barre de défilement horizontale. À moins de pouvoir changer le balisage de votre page (ce qui n’était pas mon cas), il va falloir trouver une autre solution.

Section intitulée la-solution-classique-avec-javascriptLa solution classique avec JavaScript

La première solution consiste à utiliser JavaScript. Je sais, on aimerait mieux éviter, mais cette solution a au moins le mérite de fonctionner partout et dans tous les cas. La solution consiste à calculer la taille de la barre de défilement et à la soustraire à la valeur 100vw. Je m’explique.

On obtient la largeur de la barre de défilement de la façon suivante :

const scrollbarWidth = window.innerWidth - document.body.clientWidth

Si le navigateur n’affiche pas de barre verticale, scrollbarWidth vaudra 0. Lors du chargement de la page et de son redimensionnement, on compare window.innerWidth et document.body.clientWidth. S’ils sont égaux, cela signifie qu’on n’a pas de barre de défilement. S’ils sont différents, on peut alors calculer la valeur de scrollbarWidth. On affecte ensuite la valeur de scrollbarWidth à une custom property CSS :

const getScrollBarWidth = () => {
  if (window.innerWidth !== document.body.clientWidth) {
    const scrollbarWidth = window.innerWidth - document.body.clientWidth;
    document.body.style.setProperty("--scrollbar-width",  `${scrollbarWidth}px`);
  }
}

// On appelle la fonction au chargement
getScrollBarWidth();
new ResizeObserver(() => {
  // On la rappelle lors du redimensionnement
  getScrollBarWidth();
}).observe(document.documentElement);

Enfin, on peut utiliser cette custom property pour calculer la bonne valeur de vw en CSS :

--vw: calc(100vw - var(--scrollbar-width, 0));

.full-element {
  width: calc(var(--vw) * 100);
}

Section intitulée la-solution-sans-javascript-avec-les-container-queriesLa solution sans JavaScript avec les container queries

La deuxième solution consiste à utiliser seulement du CSS (youpi), avec les container queries. Pourquoi je ne vous ai pas présenté cette solution en premier ? Parce que celle-ci n’est pas sans risques (oui, ça sent le vécu). On va pour cela faire en sorte que notre body soit un conteneur de type inline-size, ce qui correspond, en règle générale, à la largeur (je vous renvoie à l’article sur l’adaptation d’un site aux différentes langues pour comprendre ce que sont les valeurs logiques comme inline et block). Avec les container queries viennent de nouvelles unités. L’unité cqw (pour container query width) permet d’appliquer une largeur qui représente un pourcentage de la largeur du conteneur. On appliquera donc à notre élément une largeur de 100cqw.

.body {
  container-type: inline-size;
}

.full-element {
  width: 100cqw;
}

Et… tada 🎉 notre élément prend désormais la largeur du body et non pas celle du viewport ce qui supprime la barre de défilement horizontale.

Première chose à laquelle faire attention : la compatibilité. Même si les container queries (et ses unités) sont globalement bien supportés, vérifiez que cela fonctionne sur les navigateurs que vous ciblez.

Deuxième chose (et je l’ai appris à mes dépens), les container queries risquent de modifier le placement des éléments positionnés en fixe. Par défaut, les éléments fixes se positionnent par rapport au viewport. Mais dans le cas de l’utilisation des container queries, les éléments fixes se positionneront par rapport à leur conteneur, s’ils en ont un. Ainsi, si on fait en sorte que notre body soit un conteneur (comme dans l’exemple ci-dessus), tous les éléments fixes se positionneront par rapport au body et non plus par rapport au viewport. Cela risque de poser problème si on a par exemple un élément positionné en fixe en bas de notre viewport ou même une modale avec une règle inset: 0;.

Une seule solution pour cela : faire en sorte que le conteneur soit un élément du DOM suffisamment profond. On évitera de cibler le body. Il vaut mieux cibler le parent, prenant 100% de la largeur du viewport, le plus proche de notre élément. Cela limitera le risque d’avoir un élément positionné en fixe à l’intérieur de celui-ci.

Section intitulée conclusionConclusion

Les nouvelles unités de viewport ne sont pas prêtes de résoudre le problème causé par l’utilisation de la valeur 100vw. Heureusement les container queries viennent proposer une nouvelle solution, certes pas parfaite, mais sans JavaScript. Dans la mesure du possible, évitez un maximum d’utiliser cette valeur.

Section intitulée sourcesSources

Commentaires et discussions

Ces clients ont profité de notre expertise