5 avril 2020

(Médialab 3/3): Des interactions en pur CSS

Ce billet fait partie d’une suite de billet sur le projet de refonte du site web du médialab de Sciences Po.

  • Le premier billet « Une décroissance heureuse » est une introduction au projet.
  • Le deuxième billet « no fonts, no images, no javascript » montre comment ces trois points ont été mis en application.
  • Le dernier billet « Exemples de pur CSS » se concentre sur des exemples de mises en applications de principes graphiques et interactifs en pur CSS.

Comme nous l’avons vu dans un précédent billet, pour la conception du site du Médialab nous avons fait le choix d’un design graphique et interactif en pur CSS, sans JavaScript. Par cette restriction, nous avons découvert pas mal d’astuces. Nous souhaitons partager avec vous quelques lignes de code CSS qui peuvent tout à fait s’intégrer à d’autres projets.

Tout d’abord, m’important pour nous a été de bien structurer le site en HTML, et ce de manière sémantique. Il était par exemple indispensable de ne pas trop ajouter d’éléments dans le code HTML qui seraient de simples éléments visuels. Ensuite, le design du site est construit en CSS à partir de grilles, de flexbox, de variables et de media queries. Nous n’allons pas revenir sur tout le code CSS du site, ce qui serait bien fastidieux. Vous trouverez énormément de ressources en lignes sur ces propriétés.

Pour ce billet, nous avons donc choisi de concentrer cette démonstration autour de deux incontournables pour des interactions en pur CSS: l’élément <input> de type checkbox et son fidèle compagnon <label>. Ils ont été une ressource précieuse à pleins d’égard car ils nous ont permis de :

  • afficher / masquer des éléments du site;
  • créer un menu déroulant;
  • afficher des figures (en vignette dans le texte) en plein écran;
  • intervertir l’affichage de la langue;
  • trier des listes d’éléments;
  • créer un slider pour la home.

En pointillé, nous parlerons aussi des propriétés transition, animation et d’intéressants sélecteurs CSS. Puis, nous terminerons ce billet par quelques astuces en suppléments pour peaufiner les interactions.

Magic inputs

Connus pour leur utilisation dans des formulaires web, les éléments <input>, lorsqu’ils sont de type checkbox, peuvent servir à une utilisation bien plus large et peuvent être programmés afin de contrôler les fonctionnalités de la page.

En couplant leur apparition à quelques lignes de CSS, toute une panoplie de possibilités s’offre à vous. Il est par exemple possible de décrire le comportement d’autres éléments en fonction de si ces checkbox sont cochées ou non.

L’élément <label> n’est pas à négliger car il est la légende de l’input qui lui est associé. Il permet d’étiqueter une checkbox et indiquer son action. Mieux que ça, il est le moyen de styliser une nouvelle checkbox afin de lui donner une meilleure apparence et il est possible de cliquer directement dessus pour cocher ou décocher la checkbox qui lui est associée. Un <label> est associé à un <input> par l’attribut for qui renvoie à l’identifiant unique (id) de cet input.

Styliser les input

Actuellement, il n’existe pas de solution pour styliser directement les éléments <input>de type checkbox ou radio. La meilleure solution est donc de cacher les éléments <input> grâce à l’attribut hidden et de se servir des éléments <label> qui leur sont associés.

<input type="checkbox" id="myinput" hidden/>
<label for="myinput">Label</label>

L’apparence d’un <label> peut ainsi être différent en fonction de si son <input> associée est coché ou non. Nous nous sommes servis du pseudo élément ::before pour recréer une checkbox avec notre propre style.

La base: afficher / masquer un élément

Le premier exemple et le plus simple, il nous sert à afficher et masquer un élément en fonction de si une checkbox est cochée ou non.

Tout d’abord, il vous faut construire votre HTML: ajouter un <input> et son <label> juste avant l’élément que vous souhaitez afficher ou masquer :

<input type="checkbox" id="toggle-elem" name="toggle-elem" value="toggle" />
<label for="toggle-elem">Afficher / masquer l’élément</label>
<div id="elem">
  <p>Mon élément</p>
</div>

Ensuite, dans le CSS, il faut indiquer que l’élément est affiché si la checkbox est cochée et masqué si la checkbox n’est pas cochée. Pour cela nous allons nous servir de trois choses:

  • la propriété display qui définit le type d’affichage d’un élément;
  • le sélecteur CSS ~ qui permet de sélectionner les nœuds qui suivent un élément et qui ont le même parent;
  • la pseudo-classe :checked qui représente la checkbox cochée.
/* si la checkbox est cochée, n’affiche pas l’élément */
input ~ #elem {
	display: none;
}

/* si la checkbox est cochée, affiche l’élément */
input:checked ~ #elem {
	display: block;
}

Dérouler un menu

Complexifions légèrement en prenant pour exemple le cas du Médialab: dans la version du site pour les petits écrans, une partie du menu reste toujours visible (le logo) et une autre se déploie lors du clic sur l’icône en haut à droite (la navigation).

Menu du site fermé et menu déroulé sur les versions petits écrans

Dans notre exemple l’élément <label> change d’apparence en fonction de l’état du menu: lorsque le menu est fermé un icône hamburger menu est affiché, si le menu est ouvert, c’est l’icône de fermeture en forme de croix qui le sera. Ajoutons ces icônes dans notre HTML et plaçons la checkbox avant la navigation du menu qui nous voulons déployer/cacher:

<header id="topbar" role="banner">
	<div id="logo"><img src="logo.png"/></div>
	<input 	type="checkbox" id="toggle-menu" name="toggle-menu" value="menu-visibility" hidden />
	<label for="toggle-menu">
		<span class="span-nochecked" aria-label="open Menu">&#9776;</span>
 		<span class="span-checked" aria-label="close Menu">&times;</span>
 	</label>
	<nav role="navigation">
 		<ul>
 			<li><a href="#">Elem nav 1</a></li>
      <li><a href="#">Elem nav 2</a></li>
 			<li><a href="#">Elem nav 3</a></li>
     </ul>
   </nav>
</header>

Avec le CSS, nous modifions l’affichage du label avec la pseudo classe :checked. Pour l’affichage du menu, nous choisissons d’utiliser la propriété max-height plutôt que la propriété display afin que le menu se « déploie » de haut en bas.

Pour que le déploiement ne se fasse pas brutalement, nous allons utiliser la propriété transition qui permet de contrôler la vitesse d’animation lorsqu’une même propriété CSS est modifiée sur un élément.

Mettre des figures en plein écran

Certains articles du site du Médialab présentent des figures qui appuient le texte. Ce ne sont pas de simples images illustratives, elles peuvent être indispensables à la compréhension du texte ou dire quelque chose par elles-mêmes. Pour ces figures, nous n’avons donc pas appliqué de traitement graphique. De plus, nous avons mis un système d’affichage en plein écran lorsque la figure est cliquée.

Pour cela, nous avons encore une fois utilisé une checkbox. Tout d’abord, il faut créer un conteneur dans lequel sont placés dans l’ordre: un <input>, un <label> et la figure (contenant sa légende si nécessaire). Les deux premiers éléments n’étant pas indispensable pour les utilisateurs qui utilisent un lecteur d’écran, nous pouvons leur ajouter l’attribut aria-hidden="true".

<div class="container-figure">
  <input type="checkbox" id="focus-figure-1" aria-hidden="true" hidden>
  <label for="focus-figure-1" aria-hidden="true"></label>
  <figure alt="description visuelle de la figure">
    <img src="image.jpg">
  </figure>
</div>

Nous allons nous servir de la pseudo classe :checked pour changer le style de la figure lorsque la checkbox est coché. Pour cela, il faut déjà pouvoir cocher la chekbox qui est cachée, idéalement en cliquant sur l’image; le <label> doit donc se retrouver par-dessus l’image et prendre toute la largeur et la hauteur du conteneur.

/* Mettre le label par dessus l’image */
label {
  display: block;
  height: 100%;
  width: 100%;
  position: absolute;
  cursor: pointer;
  z-index: 10;
}

Il faut changer ensuite changer le style de tous les éléments contenus dans la div “container-figure” lorsque l’input est coché. Nous ne développons ici qu’un extrait du code: la figure et son image. Nous utilisons les propriétés transform et top pour centrer l’image au milieu de l’écran (nous aurions aussi pu utiliser une flexbox) et ajoutons le pseudo élément ::before à la figure pour créer un fond légèrement transparent.

/* Figure en mode plein écran */
input:checked ~ figure {
  position: fixed;
  width: 100vw;
  height: 100vh;
  top: 0;
  left: 0;
  margin: 0;
  text-align: center;
  z-index: 100;
}

/* Fond transparent en mode plein écran */
input:checked ~ figure::before {
  content: "";
  width: 100vw;
  height: 100vh;
  top: 0;
  left: 0;
  position: absolute;
  background: white;
  opacity: 0.8;
}

/* Image en mode plein écran */
input:checked ~ figure img {
    width: auto !important;
    max-width: 85vw;
    max-height: 85vh;
    top: 50%;
    transform: translateY(-50%);
  }

Intervertir la langue

Pour utiliser le sélecteur ~ , il est très important que les éléments <input> soient placé dans le DOM avant les éléments sur lesquels ils agissent. (Ces éléments peuvent être enfants d’autres éléments qui sont au même niveau que l’<input> ). Cependant, selon le design que vous souhaitez, il est parfois possible que vous aimeriez que le bouton d’action se situe après l’élément sur lequel il agit.

C’est le cas dans le site du Médialab: nous avons souhaité insérer un bandeau proposant de changer la langue de l’article tout en restant sur la même page soit disponible à la fin des articles.

Dans l’HTML, nous avons différencié les parties en anglais en français en leur attribuant des classes et grâce à l’attribut lang. L’<input> est placé avant une <div id="content"> contenant tout le texte des articles dans les deux langues. Notez que l’<input> et son <label> se trouvent donc avant la <div> de contenu où nous souhaitons échanger les deux langues.

<div id="container">
  
  <input type="checkbox" id="toggle-lang" hidden />
  <label for="toggle-lang">
    <span lang="en">See in english</span>
    <span lang="fr">Voir en français</span>
  </label>
  
  <div id="content">
    
    <article class="fr" lang="fr">
          <h1>[fr] L’univers</h1>
          <p>L’Univers est tout l’espace-temps et son contenu, 
            y compris les planètes, les étoiles, les galaxies 
            et toutes les autres formes de matière et d’énergie.</p>
     </article>
    
    <article class="en" lang="en">
      <h1>[en] The universe</h1>
      <p>The Universe is all of space and time and their contents, 
        including planets, stars, galaxies, 
        and all other forms of matter and energy.</p>
    </article>
  
  </div>
 
</div>

Pour mettre visuellement le bandeau après le contenu, nous avons échangé l’ordre des éléments en créant une flexbox et en utilisant la valeur column-reverse dans la propriété flex-direction. (Vous pouvez aussi utiliser la propriété order si vous ne voulez changer la place que d’un seul élément du conteneur.)

Ensuite, pour masquer/ afficher les éléments en fonction de leur langue, nous avons utilisé la propriété display (comme nous l’avons déjà vu) mais en sélectionnant les éléments directement par leur attribut lang.

Trier une liste d’éléments

Dans le site du médialab, quatre pages contiennent des listes de tri plus ou moins complexes: la page des actualités, celle des activités, celle des productions, et celle de l’équipe. Nous allons voir comment trier une liste selon des choix multiples en prenant pour exemple la page équipe.

Tout d’abord l’HTML: dans la liste qui sera triée, il est attribué à chaque personne une class indiquant si le membre est associé ou non ainsi que son domaine (dans le code ci-dessous, simplifié par des couleurs). Vous remarquez que la classe est composée des deux éléments séparés par un tiret.

Au-dessus de cette liste apparaissent les checkbox corresponsant à chacune des possibilités de tri. Les <input> sont cachés et au même niveau que la liste. Les <label> sont séparés en deux groupes distincts (mais ce n’est pas indispensable).

<input type="checkbox" id="input_members" class="input_membership" name="membership" value="member" hidden />
<input type="checkbox" id="input_associates" class="input_membership" name="membership" value="associate"hidden />

<input type="checkbox" id="input_red" class="input_color" name="color" value="red" hidden />
<input type="checkbox" id="input_green" class="input_color" name="color" value="green" hidden />
<input type="checkbox" id="input_blue" class="input_color" name="color" value="blue" hidden />
<input type="checkbox" id="input_yellow" class="input_color" name="color" value="yellow" hidden />

<div class="group-filters">
<h1>Filter by membership</h1>
<label for="input_members" id="input_members_label">Members</label>
<label for="input_associates" id="input_associates_label">Associates</label>
</div>

<div class="group-filters">
<h1>Filter by domains</h1>
<label for="input_red" id="input_red_label">Red</label>
<label for="input_green" id="input_green_label">Green</label>
<label for="input_blue" id="input_blue_label">Blue</label>
<label for="input_yellow" id="input_yellow_label">Yellow</label>
</div>

<ul id="list">
  <li class="member-red">Member red</li>
  <li class="member-green">Member green</li>
  <li class="member-blue">Member blue</li>
  <li class="member-yellow">Member yellow</li>
  <li class="associate-red">Associate red</li>
  <li class="associate-green">Associate green</li>
</ul>

Si nous utilisons la même technique que jusque’à présent, nous nous heurtons à un problème: lorsque qu’aucune checkbox n’est sélectionnée, aucun élément de la liste n’apparaît. Nous pourrions choisir que toutes les checkbox soit sélectionnées au départ et que l’utilisateur doivent les décocher pour trier sa liste mais ce serait un terrible choix de design.

Pour remédier à cela, nous avons laissé les éléments apparents par défaut et nous les avons cachés aussitôt qu’un élément est sélectionné; puis, nous avons écrit les déclarations CSS liés aux checkbox cochées.

Il existe d’ailleurs une astuce pour sélectionner tous les éléments dont la class contient un certain mot. Le code [class*="red"], sélectionnera tous les éléments contenant le mot « red ». (cf. l’article Attribute selectors)

input:checked ~ #list li {
  display: none;
}

#input_members:checked ~ #list [class*="member"]{ 
  display: block;
}

#input_associates:checked ~ #list [class*="associate"]{ 
  display: block;
}

#input_red:checked ~ #list [class*="red"] { 
  display: block;
}

#input_green:checked ~ #list [class*="green"] { 
  display: block;
}

#input_blue:checked ~ #list [class*="blue"] { 
  display: block;
}

#input_yellow:checked ~ #list [class*="yellow"] { 
  display: block;
}

À cette étape, un nouveau problème est apparu. Si l’on sélectionne les « members » (tri par membership) puis les « red » (tri par domaine/couleur), on s’attend à trouver les « member red »; or on se retrouve non seulement avec tous les membres de la liste affichée mais aussi les « associate red ».

Il nous a docn fallu continuer notre CSS et spécifier les croisements. Le code CSS utilisé devient beaucoup plus complexe; nous n’avons pas la place de développer ici mais il est disponible sur codepen.

Sur la page productions le tri devient plus complexe car les productions sont classés en groupe et sous-groupes. Si nous gardons le tri avec des <input>, le code risque de s’allonger et devenir trop complexe (voir impossible) à écrire. Nous avons donc envisagé une autre solution: chaque groupe de production possède sa propre page de listing créée lors de la compilation du site. Les sous-groupes sont quant à eux triés grâce aux checkbox. Il est cependant à noter qu’avec une séparation en page, il n’est plus possible d’afficher plusieurs groupes de production en même temps. C’est pour autant, la solution la plus efficace que nous ayons trouvé.

Peaufiner son scroll et ses survols

Nous terminons notre billet sur quelques astuces pour peaufiner et fluidifier les interactions autour du scroll et des survols d’éléments.

Scroll

Sur le site du médialab, nous avons utilisé plusieurs fois des ancres (des liens vers les id de certains éléments) pour amener l’utilisateur à un certain niveau de la page lorsque celle-ci est trop longue. Par exemple, sur la page des productions, les productions apparaissent par ordre antéchronologique. Un menu sur la gauche permet de sélectionner l’année à laquelle on souhaite aller. Lors du clic sur l’année, la page défile (scroll) automatiquement pour arriver au bon endroit de la liste.

Pour que ce défilement ne soit pas brutal, nous avons utilisé la déclaration scroll-behavior: smooth; sur l’ensemble du site; c’est-à-dire dans le body.

Il est aussi à noter que lors de l’utilisation des ancres, l’élément sélectionné se retrouve en haut de la page. Or, nous avons un menu qui prend de la place en haut du site. L’élément ancré risquait donc d’être caché par le menu. Pour remédier à cela, nous avons utilisé la pseudo-classe :target qui permet de cibler l’élément dont l’attribut id correspond au fragment d’identifiant de l’URI du document.

Par exemple, dans l’URI https://medialab.sciencespo.fr/productions/#year-2016, c’est l’élément avec l’identifiant unique year-2016 qui est ancré.

Il suffit alors d’ajouter une marge à l’élément ancré. La hauteur de la marge est égale à la hauteur du menu:

.list-year:target {
	margin-top: var(--height-header);
}

Survol

Dans l’ensemble du site (hors versions petits écrans), les images d’illustrations restent en partie cachées et ne se dévoilent qu’au survolement de la souris.

L’effet est en réalité très simple. Dans son état normal, l’image possède une largeur maximum et lors du survol, cette largeur est agrandie. Pour cela, nous avons utilisé la pseudo-classe :hover qui indique le style d’un élément lorsqu’il est survolé. La propriété transition permet de donner un effet plus fluide au dévoilement. Cette méga propriété en englobe plusieurs autres tout comme la propriété border englobe border-width, border-style et border-color. Avec transition, vous pour spécifier:

  • la propriété à animer en la déclarant explicitement (transition-property), dans notre cas la largeur maximum;
  • la durée de transition (transition-duration);
  • l’effet de la transition (transition-timing-function) avec plusieurs valeurs préféfinies proposées (ease, linear, ease-in-out, etc.) ou en paramétrant une courbe spécifique avec la fonction cubic-bezier.
.image-pre {
  max-width: 30px;
  overflow: hidden;
  transition: max-width 0.25s cubic-bezier(1, 0, 0, 1)
    
}
.image-pre:hover{
  max-width: 100%;
}

CSS is really awesome

Ainsi s’achève notre étude de cas et notre série de billets sur les coulisses du site du médialab. S’il ne fallait retenir qu’une chose, c’est qu’un recours quasi-exclusif au CSS peut se montrer tout aussi efficace qu’un recours à JavaScript pour rendre un site interactif. Et surtout, c’est bien plus amusant et gratifiant à pratiquer.

L’approche n’est pas tant fastidieuse et complexe si l’on accepte d’abandonner l’idée que seul JavaScript serait un langage permettant les interactions utilisateurs. Le secret est de se plonger entièrement dans CSS et mieux découvrir sa grammaire et ses conjugaisons– un peu comme dans l’apprentissage d’une langue étrangère.

Vous l’aurez compris, c’est la façon de construire la page, de structurer le document et même l’ensemble du site qui est chamboulé par ce choix. Subsiste désormais cette question. Quelle est, entre cent lignes de CSS immuable et une de JS cent fois corvéable, la solution la moins énergivore et la plus performante ?

Billet écrit à quatre mains par Julie et Benjamin