Créons, ensemble, des sites qui feront le bonheur de vos utilisateurs
(Expérimental)

Résultats trouvés

Commencez à taper quelque chose, la recherche se déclanchera toute seule !

Basculer un site en lecture horizontale avec CSS Column

Suite à mes derniers articles sur les nouveautés de mise en page apportées par CSS3, l'envie de les mettre en pratique sur ce site me démangeait de plus en plus. Ayant un peu de temps, et personne pour m'empêcher de le faire, j'ai donc implémenté une lecture horizontale pour ce site lorsqu'il est consulté sur écran larges. Et bien évidemment de vous en faire un retour sous forme d'article.

Pourquoi une lecture horizontale ?

Depuis longtemps je souhaitais tester la possibilité de basculer un site en lecture horizontale sur les écrans ayant une largeur suffisamment importante. L'idée en elle-même vient de l'ergonomie présente sur les tablettes Windows 8 où toute la consultation de fait sur les côtés et non pas vers le bas comme sur les autres plateformes.

Menu Windows 8 RT

Ayant trouvé l'approche très intéressante et ingénieuse pour exploiter au mieux l'espace disponible sur ces périphériques, utilisés en mode paysage, je m'étais demandé s'il était possible de réaliser la même chose sur un site web...

Un autre objectif est également d'afficher plus de contenu par écran. Les règles typographiques préconisent des longueurs de ligne contenant entre 40 et 80 caractères pour une lecture optimale. Par conséquent, sur un écran large les lignes ne sont pas plus longues que sur les autres écrans afin de maintenir une bonne lisibilité.

En basculant en lecture horizontale avec des colonnes, il est beaucoup plus facile de respecter ces règles sans augmenter la taille de police et sans avoir de grosses marges vides sur les côtés. En l'occurrence l'équivalent d'une vingtaine de lignes en plus sur mon écran (1600x900).

Capture du site en lecture verticale

Capture du site en lecture horizontale

Une réalisation loin d'être évidente...

Par nature le web est conçu pour fonctionner verticalement, les blocs ne dépassent pas de la page en largeur. Il est possible de contourner le problème en définissant des tailles fixes et des positionnements relatifs et absolus. Mais en général ces techniques sacrifient l'approche responsive et ne sont donc pas optimales.

Grâce à CSS Column, il est enfin possible d'avoir un débordement horizontal naturel. La norme est prévue pour ajouter autant de colonnes nécessaires si on fixe la hauteur d'un bloc.

Reste à mettre à l'épreuve cette norme très peu utilisée, de régler les problèmes de navigations éventuelles et d'ajuster la mise en page pour que l'ensemble reste lisible et le tour est joué !

Affichage en colonnes avec CSS Column

La norme CSS Column permet d'afficher le contenu d'un bloc sous forme de colonnes similaires à celles utilisées dans la presse. Cette norme est relativement basique et est plutôt bien supportées sur les navigateurs actuels.

Support de CSS Columns sur caniuse.com

Pour basculer en affichage colonne, il vous suffit d'ajouter la propriété :

columns: <nombre de colonne> < largeur des colonnes>;

Ou de la séparer en deux selon vos besoins :

column-count: <nombre de colonne>;
columns-width: < largeur des colonnes>;

Premier petit problème, bien qu'implémentée depuis longtemps, la propriété est toujours préfixée dans tous les navigateurs à l'exception d'Internet Explorer. Ce qui nous donne :

-webkit-columns: <nombre de colonne> < largeur des colonnes>;
-moz-columns: <nombre de colonne> < largeur des colonnes>;
columns: <nombre de colonne> < largeur des colonnes>;

Note : dans la suite de l'article je n'utiliserai que la syntaxe sans préfixe pour simplifier la lecture.

Il est important de noter que les valeurs fournies ne sont qu'indicatives et le navigateur fait au mieux pour respecter ces exigences en fonction de la largeur disponible.

Après de rapides calculs, une largeur de colonne de 450 pixels me permet d'afficher 2 colonnes de 50 caractères sur une largeur de 1280 pixels ce qui me semble un bon compromis entre la largeur de ligne optimale et la quantité d'information affichée.

Ce qui nous donne :

columns: 2 450px;

N'ayant pas encore fixé la hauteur, nous pouvons nous attendre à un contenu de 900 pixels de large comprenant deux colonnes de même hauteur.

Si cela fonctionne comme prévu sur Internet Explorer et Firefox, ce n'est pas le cas sur Chrome...

Sur Chrome, la largeur par défaut du bloc, à 100%, prend le dessus faisant passer celui-ci sous le menu latéral. Autre effet de bord de cette largeur à 100%, nous avons deux colonnes mais chacune occupant 50% de l'espace disponible. Pour avoir le comportement désiré, il nous faut forer la largeur maximale à 900 pixels :

max-width: 900px;

Rendu du site avec deux colonnes

Petit détail étonnant au passage, les colonnes ne font pas la même hauteur :

  • Internet Explorer : 6016 pixels
  • Firefox : 6131 pixels
  • Chrome : 6335 pixels

Ce n'est pas très grave puisque justement l'étape suivante est de fixer la hauteur de notre contenu afin qu'il n'y ait plus de défilement vertical sur notre page.

Basculer en défilement horizontal

La spécification de CSS Column précise que si la hauteur du bloc contenant les colonnes est fixées, le débordement se fait sur le côté en répétant autant de colonnes que nécessaire pour que tout le contenu soit affiché.

Cela tombe bien, c'est exactement ce que nous souhaitons pour notre lecture horizontale.

Pour fixer la hauteur maximale de notre contenu, nous allons utiliser les nouvelles unités introduites en CSS3 :

  • vw : Représente un pourcentage de la largeur du viewport
  • vh : Représente un pourcentage de la hauteur du viewport
  • vmin : Représente un pourcentage de la valeur minimale entre la largeur et la longueur du viewport
  • vmax : Représente un pourcentage de la valeur maximale entre la largeur et la longueur du viewport

Pour faire très simple, le viewport représentant la surface d'affichage disponible. Il est donc indépendant du contenu mais change en fonction de la taille de la fenêtre contenant le navigateur.

Illustration du viewport

Pour fixer la hauteur à 100% de la hauteur de la fenêtre, il nous suffit d'ajouter la propriété :

height: 100vh;

Comme prévu, les colonnes débordent sur la droite :

Colonnes avec hauteur fixe

Cependant, comme vous pouvez le voir il reste une barre de défilement verticale. Cela est dû aux marges mais surtout au fait que les unités basées sur le viewport ignorent les barres de défilement.

Il faut donc ajuster un peu la hauteur à l'aide de la fonction de calcul, elle aussi introduite en CSS3 :

height: calc(100vh - 4rem - 22px;

La marge évitant que le contenu soit collé en haut de l'écran faisant 4rem et 22px à la marge nécessaire pour les différentes barres de défilement des navigateurs. Je suis tombé sur cette valeur en testant sur les différents navigateurs que j'avais sous la main.

Pour être certain de ne pas avoir de débordement vertical, il est plus sur d’ajouter un contrôle de l’overflow sur la balise body :

body {
   overflow-y : hidden ;
}

Nous obtenons enfin le rendu attendu avec une navigation purement horizontale :

Colonnes avec hauteur fixe final

Ajuster la mise en page

Maintenant que notre lecture horizontale est prête, il est temps d'adapter un peu la mise en page.

Premier élément à ajuster est l'espacement entre chaque colonne. Il existe pour cela une propriété permettant de définir la largeur des gouttières :

column-gap: <largeur>;

En fixant l'espacement à 40px, les colonnes s'espacent bien, cependant il reste un souci sur la dernière :

La dernière colonne est collée au bord de l'écran

La propriété column-gap n'agissant que sur l'espace entre deux colonnes, le résultat est normal. Et comme nous avons dû fixé la largeur de notre bloc à la taille de 2 colonnes pour que tout fonctionne bien, impossible de lui donner une marge...

La solution est d'encapsuler notre contenu dans un autre bloc auquel on applique une marge au lieu d'utiliser les gouttières prévus dans la norme CSS Column.

Nous passons donc d'une structure de ce type :

<div class="columns">
Mon contenu !
</div>

à :

<div class="columns">
	<div class="content">
	Mon contenu !
    </div>
</div>

Avec les classes correspondantes :

.columns {
	columns: 2 450px;
	column-gap: 0;
	max-width: calc(900px + 4rem);
}

.content {
	margin-right: 4rem;
}

Il nous faut ajouter la taille de la marge à notre largeur de base et les colonnes ayant un espacement par défaut, nous le remettons à 0 pour que seule notre marge entre en compte.

Nous obtenons ainsi le résultat suivant :

La dernière colonne avec une marge

Un dernier point qui pose problème est le manque de contrôle sur les sauts de colonnes. Rien de plus énervant de devoir changer de colonne après la première ligne du paragraphe, au milieu d'une liste ou d'un bloc de code.

Par exemple :
Des changements de colonnes intempestifs

Heureusement la spécification à prévu ce cas en réutilisant les propriétés dédiées à la gestion des sauts de pages dans les CSS dédiées à l'impression : break-before, break-after et break-inside. Elle y ajoute des valeurs spécifiques aux colonnes : column et avoid-column en plus des valeurs classiques.

Par exemple si vous souhaitez éviter d'avoir un saut de colonne au milieu d'un paragraphe, la syntaxe est :

.columns p {
	break-inside: avoid-column;
}

Si vous souhaitez avoir un saut de page systématique avant chaque titre de niveau 2 :

.columns h2 {
	break-before: column;
}

Pour le moment ces propriétés ne sont supportées que par Internet Explorer. Les navigateurs basés sur Blink et Webkit, tels que Chrome, ont des propriétés équivalentes spécifiques : -webkit-column-break-before, -webkit-column-break-after et -webkit-column-break-inside. Elles prennent les valeurs always et avoid pour respectivement forcer et éviter un saut de colonne.

Ce qui, après avoir fixé les éléments souhaités, nous donne le résultat suivant :

Les changements de colonnes intempestifs ont disparus

J'en ai profité au passage pour que les balises hr forcent un changement de colonne au lieu d'afficher un séparateur. Cela me permet d'avoir un contenu indépendant de la mise en page normale ou en colonnes. Il bascule entre un trait de séparation et un saut de colonne.

Avoir un site qui se lit horizontalement c'est bien joli mais les utilisateurs ont l'habitude de naviguer dans un site de manière verticale. Et qui dit habitude dit reflexes et matériels adaptés...

Pour les interfaces tactiles il ne devrait pas y avoir de soucis. Balayer vers la droite plutôt que vers la gauche est instinctif et naturel une fois que l'utilisateur a repéré le sens de navigation.

Sur les ordinateurs ayant un trackpad ou une souris, c'est beaucoup moins évident. Surtout que la majorité des souris n'ont pas de molette permettant de défiler horizontalement...

Il me faut donc un moyen de faire défiler le site horizontalement lorsque l'utilisateur utilise la molette verticalement. Et ça tombe bien, HTML5 a ajouté un évènement wheel permettant d'intercepter l'utilisation de la molette de la souris.

document.addEventListener('wheel', columnModeWheelEventManager);

L'évènement possède de nombreuses propriétés pour savoir ce qui s'est passé au moment de l'utilisation de la molette. Il est par exemple possible de savoir si une touche spéciale (control, alt, shift) ou un bouton de la souris était appuyé au moment du scroll. Cette information est importante car nous ne voulons intercepter que le défilement sans modifier les autres comportements. Par exemple, si vous appuyez sur la touche control pendant que vous utilisez la molette la page zoom...

L'évènement permet également de savoir sur quel axe, dans quel sens et l'équivalent de la distance parcourue lorsque la molette a été utilisée. En l'occurrence nous ne voulons intercepter que le défilement sur l'axe Y. Nous pouvons connaître le delta à l'aide du paramètre deltaY. S'il est négatif la page défile vers le haut, s'il est positif vers le bas.

Reste un petit souci, il existe plusieurs unités possibles pour la valeur renvoyée par la propriété deltaY : pixels, lignes ou pages. Il est possible de connaitre l'unité utilisées à l'aide de la propriété deltaMode. Par exemple, Internet Explorer et Chrome renvoient une valeur en pixels tandis que Firefox renvoi une valeur en lignes.

Il nous faut donc convertir des lignes en équivalent pixels qui collent mieux à notre défilement horizontal. Après plusieurs tests j'ai fini par retenir empiriquement la valeur 40. Cette valeur dépend probablement de votre contenu, tailles de police, etc.

Enfin, il est possible de faire défiler la fenêtre à l'aide de la fonction scrollBy() présente sur l'objet window. Cette fonction prend en paramètre un delta sur l'axe des X et un delta sur l'axe des Y. Ce qui est parfait puisque nous avons récupéré un delta juste avant... Il ne reste qu’à changer l'axe sur lequel l'appliquer.

Au final, la fonction gérant la molette est :

function columnModeWheelEventManager(event) {
    var delta;
    if(!event.altKey
    && !event.ctrlKey
    && !event.metaKey
    && !event.shiftKey
    && event.button == 0
    && event.deltaY != 0) {
        event.preventDefault();
        delta = event.deltaY;
        if(event.deltaMode == 1) {
            delta = delta * 40; // ajustement pour les valeurs en lignes
        }
        window.scrollBy(delta,0);
    }
}

Tout semble bon, sauf qu'il y a un bug étrange sur Chrome, à partir de la troisième colonne, si le curseur se trouve sur la marge entre deux colonnes, l'évènement wheel n'est pas déclenché... Partout ailleurs il fonctionne... Dès que j'arriverai à l'isoler plus précisément, je remonterai le bug à l'équipe de Chromium. En attendant ce n'est vraiment pas optimum sur ce navigateur…

Note : De manière générale, il est fortement déconseillé de modifier le comportement du défilement natif. En l'occurrence, il s'agit d'une expérimentation donc je ne me suis pas gêné. Mais si vous devez intercepter le défilement sur votre site réfléchissez-y à deux fois et testez de manière très poussée !

Déclencher la bascule en colonne au bon moment

Maintenant que tout est prêt, reste le choix crucial pour une fonctionnalité aussi expérimentale de quand l'activer...

La première chose est de la réserver aux écrans suffisamment larges à l'aide d'une média query adaptée. Pour le moment, je me suis uniquement basé sur une largeur minimale de 1280 pixels. Les média queries permettant de détecter les modes paysage ou les ratios largeurs/hauteurs, j'aurai également pu ajouter ces critères.

@media only screen and (min-width: 1280px) {
   ... le CSS dédié aux écrans larges...
}

Il me faut également détecter les navigateurs supportant les colonnes, pour cela le code JavaScript est assez trivial :

function supportColumns() {
   return 'columns' in document.body.style 
       || 'WebkitColumns' in document.body.style
       || 'MozColumns' in document.body.style;
}

Ne voulant pas activer automatiquement le mode colonnes, la détection du support affiche un bouton sous le menu. Ce bouton permettant de faire la bascule entre les deux modes. Et grâce au localStorage je peux garder en mémoire la configuration choisit par le lecteur d'une page à l'autre et, surtout, d'une visite à l'autre.

Ce qui nous donne le code suivant dans l'entête de la page :

var columnMode = false ;
function toggleColumnMode() {
    if(columnMode) {
        columnMode = false;
        document.body.classList.remove('columns');
        window.localStorage.setItem('columns',0);
        document.removeEventListener('wheel', columnModeWheelEventManager);
    } else {
        columnMode = true;
        document.body.classList.add('columns');
        window.localStorage.setItem('columns',1);
        document.addEventListener('wheel', columnModeWheelEventManager);
    }
}

if(supportColumns() && window.localStorage.getItem('columns') == 1) {
    toggleColumnMode();
}

J’ajoute une class columns sur la balise body qui me sert à n’appliquer les styles liés à l’affichage en colonnes que si elle est présente. Pui j’ajoute la bonne valeur dans le localStorage et le listener pour la molette.

Et, après le chargement de la page (ici dans l'évènement ready de jQuery vu qu'il est sur le site), l'affichage du bouton :

if(supportColumns()) {
    $('.column-switch').css('display','block');
}

Mais, sur les navigateurs supportant les colonnes, notre bouton apparaitra même si notre largeur est inférieure à 1280 pixels de large. Pour remédier à ce problème nous allons utiliser la fonction matchMedia introduite encore une fois en HTML5 et permettant de valider les média queries en JavaScript.

Cette fonction s'utilise en deux temps. Tout d'abord on initialise notre média query en l'appelant de cette manière :

var columnMedia = window.matchMedia('only screen and (min-width: 1280px)');

La variable columnMedia contient un objet de type MediaQueryList permettant de vérifier si la média query est vérifiée ou non avec l'attribut matches. Mais encore mieux, cet objet peut nous prévenir lorsque la valeur de matches change à l'aide de la méthode addListener. Cette méthode prenant un object MediaQueryList en paramètre.

Cela nous donne le code suivant :

var displayColumnButton = function(mediaQueryList) {
	if(supportColumns() && mediaQueryList.matches) {
		$('.column-switch').css('display','block');
	} else {
		$('.column-switch').css('display','');
	}
};

columnMedia.addListener(displayColumnButton);

displayColumnButton(columnMedia);

La fonction displayColumnButton vérifie si la média query est respectée et affiche ou masque le bouton en fonction de la valeur. On ajoute le listener pour detecter les changements de dimensions du navigateur via un redimensionnement de la fenêtre ou un changement d'orientation. Enfin on appelle une première fois la méthode en lui passant l'objet initial pour gérer l'affichage initial de la page.

Et voilà, une navigation horizontale expérimentale qui apparait uniquement sur certains types d'écrans, sur les navigateurs supportant la fonctionnalité et que l'utilisateur peut activer selon ses préférences !

Conclusion

L'implémentation de base est finalement relativement simple. Mais elle reste intéressante car elle permet de voir comment sortir des sentiers battus en utilisant plusieurs nouveautés introduites par CSS3 et HTML5 : CSS Columns, localStorage, évènement wheel, média queries en CSS en JavaScript, les unités de viewport et la fonction CSS calc().

Loin d'être si évident au début puisqu'elle allait à l'encontre du fonctionnement vertical habituel des navigateurs, il est tout à fait possible de créer de nouvelles manières d'utiliser les sites pour exploiter au mieux la place disponible. Le contenu lui ne changeant pas, ce qui respecte parfaitement l'approche Responsive Web Design !

Au final j'y ai passé environ une douzaine d'heure, sans compter l'écriture de cet article, pour un résultat qui me semble suffisamment bon pour le proposer sur mon site.

Comme vous avez pu le voir, l'approche reste encore expérimentale. Entre les bugs, les fonctionnalités non encore implémentées partout, les propriétés spécifiques, il est difficile de prévoir ce qui va se passer sur les différents navigateurs et dans les différents cas de figure.

Je compte sur vous pour me remonter les problèmes et me faire des suggestions dans les commentaires pour améliorer le concept.

Dans tous les cas n'hésitez pas à jouer avec ces nouvelles propriétés !


Pour aller plus loin :

Autour du sujet :

Découvrez les formations HumanCoders sur CSS3 avancé, HTML5 et le Responsive Web Design

Humancoders

Ou invitez moi pour un BBL sur les dernières nouveautés HTML5/CSS3 ou sur comment booster vos mises en pages avec CSS3 : http://www.brownbaglunch.fr