Quantity Queries pour CSS

Dans cet article déjà fameux, Heydon Pickering démontre comment on peut créer des queries basées sur les quantités en pur CSS en utilisant ingénieusement pseudo-classes et combinateurs. Absolument génial Heydon!

Par

Vous aussi vous détestez les documentaires qui ne tiennent pas leurs promesses ? Avec leurs titres aguicheurs, genre À la Recherche du Calmar Géant et un teasing d’enfer avec des images de formes étranges dans les eaux troubles et des scientifiques tout excités qui pointent du doigt vers le large. Vous vous installez pour regarder et vous vous dites en fronçant les sourcils “ya intérêt que je voie des calmars, sinon je leur envoie un courrier à ma sauce”.

Et bien sûr, après 90 minutes d’interviews de pêcheurs blasés, le présentateur est bien obligé de conclure que “Non… non, nous n’avons pas trouvé de calmars géants… Mais peut-être… un jour [majestueuse envolée orchestrale]”. Génial. Vous vouliez Trouver Nemo et vous avez eu On n’a pas trouvé Nemo à la place.

Je ne voudrais pas vous infliger cela, Chers Amis.

Ce qui suit est un guide de création de points de ruptures de style pour les quantités d’éléments HTML, assez semblable à ce que vous faites déjà avec les @media queries pour les dimensions de viewport. Je ne pointerai pas du doigt vers quelque spécification floue dans le lointain, nous allons réaliser ce guide aujourd’hui, avec le CSS dont nous disposons.

Contenu dynamique

La préoccupation première du design responsif est l’espace. Lorsque nous testons des layouts responsifs, nous prenons une certaine quantité de contenu et nous voyons dans quels espaces elle va pouvoir s’insérer au mieux. Le contenu est supposé constant, l’espace variable.

La @media query est la petite chérie du design responsif parce qu’elle nous permet d’insérer des “points de rupture” (breakpoints) là où notre stratégie de mise en page cesse d’être viable et doit être remplacée par une autre. Cependant, il n’y a pas que les dimensions du viewport qui mettent la pression sur l’espace, il y a également la quantité de contenu.

Si nos utilisateurs finaux peuvent utiliser tous types de terminaux, de la même manière nos rédacteurs de contenus peuvent ajouter et supprimer du contenu. C’est à cela que servent les CMS. C’est aussi ce qui rend les maquettes Photoshop doublement obsolètes : ce sont des instantanés d’un viewport unique, montrant un état de contenu unique.

Dans cet article, je vais présenter une technique permettant à CSS de prendre en compte la quantité, grâce à des sélecteurs spécialement associés. J’appliquerai ces sélecteurs en particulier à un problème très classique : comment modifier l’affichage d’items dans un menu de navigation horizontal lorsqu’il y a trop d’items par rapport au mode d’affichage initialement prévu. Pour le dire autrement, je vais montrer comment passer d’un layout display: table-cell à un layout display: inline-block lorsque le nombre d’items est “supérieur ou égal à 6”.

Je n’utiliserai pas de JavaScript ou de logique de template et le markup de notre liste de menu ne comportera pas de classes. En utilisant CSS uniquement, cette technique respecte le principe de la séparation des contenus, selon lequel le contenu (HTML) et la présentation (CSS) ont des rôles clairement définis. Le layout est le job de CSS et, à chaque fois que c'est possible, uniquement de CSS.

Comparaison du layout initial pour moins de 6 items avec le layout pour 6 items ou plus.

La démonstration est disponible sur ce CodePen auquel je me réfèrerai tout au long de cet article.

Pour figurer la quantité, j’emploierai des diagrammes de calmars représentant les éléments HTML. Des calmars verts et une hirondelle représentent les éléments qui correspondent au sélecteur CSS en question, les calmars rouges avec une croix sont les éléments non sélectionnés et les calmars gris représentent les éléments qui n’existent pas.

Illustration des calmars symboliques. Un vert pour les éléments sélectionnés, un rouge pour les éléments non sélectionnés, un gris pour les éléments inexistants.

Décompte

Pour déterminer la quantité d’éléments dans un contexte donné, nous devons les compter. CSS ne nous offre pas d’API de décompte explicite, mais nous pouvons résoudre ce problème avec une combinaison inventive de sélecteurs.

Compter jusqu’à un
Le sélecteur :only-child offre un moyen de styler des éléments s’ils apparaissent de manière isolée. Il nous permet de “styler tous les éléments enfants d’un élément donné si le décompte de ces enfants est égal à un”. Avec son compagnon d’écurie :only-of-type, c’est le seul sélecteur simple dont on puisse dire qu’il est basé sur une notion de quantité.

Dans l’exemple qui suit, j’utilise :only-of-type pour ajouter un style spécifique à tous les boutons qui sont les seuls éléments de leur type parmi les éléments frères (sibling) et je donne à ces boutons solitaires une plus grande font-size parce que leur singularité leur donne de l’importance.

button {  
    font-size: 1.25em;
}

button:only-of-type {  
    font-size: 2em;
}

C’est ici la partie cruciale. Si nous commençons avec un seul bouton, rempli avec une font-size plus grande, et que nous ajoutons des boutons avant et après lui, chaque bouton aura alors une font-size plus petite. Le style de tous les éléments dépend d’une limite de deux : s’il y a “moins de deux” éléments, c’est la taille de police la plus grande qui est appliquée. Regardez à nouveau le code en gardant la notion de “moins de deux” à l’esprit :

button {  
    font-size: 1.25em;
}

button:only-of-type {  
    font-size: 2em;
}
Avec la logique “moins de deux”, un élément sélectionné (calmar vert) devient deux éléments non sélectionnés (calmars rouges) lorsqu’un élément est ajouté.

Vous pouvez également inverser la logique CSS, si cela vous paraît plus naturel, en utilisant la négation :not(), la condition devenant alors “plus d’un” :

button {  
    font-size: 2em;
}

/* "Plus d’un" a pour conséquence une font size plus petite */

button:not(:only-of-type) {  
    font-size: 1.25em;
}
La logique “plus d’un” a pour conséquence qu’un élément non sélectionné (calmar rouge) devient deux éléments sélectionnés (calmars verts) lorsqu’un élément est ajouté.

Quantité N
C’est évidemment très sympa de styler des éléments à partir de la logique de “plus d’un” ou de “moins de deux”, mais une interface flexible basée sur la quantité doit pouvoir accepter n’importe quelle quantité. Autrement dit, je devrais pouvoir appliquer un style “supérieur ou égal à n” pour n’importe quelle valeur de n. Je pourrais alors appliquer une style “supérieur ou égal à 6” à notre menu de navigation.

Avec cette finalité en tête, essayons d’abord d’appliquer des styles à des quantités discrètes comme “exactement 6 en tout” ou “exactement 745”. Comment faire ? J’aurais besoin d’un sélecteur me permettant de traverser des ensembles d’éléments quel que soit leur nombre.

Heureusement, le sélecteur :nth-last-child(n) accepte le nombre “n”, ce qui me permet de compter les éléments depuis la fin en remontant vers le début. Par exemple, :nth-last-child(6) correspond à l’élément qui est le sixième à partir de la fin parmi les éléments frères.

Les choses commencent à devenir intéressantes lorsque l’on concatène :nth-last-child(6) avec :first-child, ce qui introduit une deuxième condition. De cette façon, je cherche tout élément qui soit à la fois le sixième élément depuis la fin et le premier élément depuis le début.

li:nth-last-child(6):first-child {  
    /* les styles appliqués aux calmars verts */
}

Si un tel élément existe, alors l’ensemble auquel il appartient comprend exactement six éléments. D’une manière un peu radicale, j’ai écrit un CSS qui me dit combien d’éléments je suis en train d’observer.

Des six calmars, le premier est vert, les autres sont rouges. Le premier est ciblé par le sélecteur :nth-last-child(6) et par le sélecteur :first-child.

Tout ce qu’il me reste à faire, c’est de m’appuyer sur cet élément-clé pour appliquer un style aux autres éléments de l’ensemble. Pour cela, j’utilise le combinateur de voisins généraux (general sibling). ( NdT : Pour tout savoir sur les combinateurs et les pseudo-éléments, consultez l’article Combinateurs et Pseudo-Classes CSS.)

Nous avons maintenant six calmars verts parce que le premier calmar vert est associé au combinateur general sibling pour transformer tous les calmars rouges qui le suivent en calmars verts.

Si vous n’êtes pas familier du combinateur general sibling, le ~li (tilde li) à la fin de li:nth-last-child(6):first-child ~ li signifie “cibler tous les éléments li qui suivent li:nth-last-child(6):first-child”. Dans l’exemple suivant, les éléments prennent tous une couleur de police verte si leur nombre total est exactement égal à six :

li:nth-last-child(6):first-child,  
li:nth-last-child(6):first-child ~ li {  
    color: green;
}

Supérieur ou égal à 6

Il n’est pas très utile de cibler une quantité discrète car cela ne peut que viser une situation spécifique. De la même manière, l’utilisation de largeurs déterminées dans nos media queries, plutôt que min-width ou max-width serait peu efficace :

@media screen and (width: 500px) {
    /* applique des styles pour des viewports de 500px de large exactement */
}

Dans notre menu de navigation, ce que je veux c’est changer de layout lorsque j’atteins un certain seuil : une quantité charnière. Je veux changer à partir de 6 items ou plus — pas exactement ni uniquement à 6. Lorsque j’atteins ce seuil, je veux passer d’une table à une configuration inline-block plus simple. Plus important, je veux conserver cette configuration lorsque le nombre d’items augmente.

La question est donc : par où commencer pour construire ce sélecteur ? C’est une question de décalages (offsets).

L’argument N+6
Un autre type d’arguments arithmétiques accepté par le sélecteur :nth-child() prend la forme “n + [entier]”. Par exemple, nth-child(n+6) applique un style à tous les éléments d’un ensemble en commençant par le sixième (pour n=0, puis le septième pour n=1, etc.)

Un ensemble de calmars rouges qui deviennent verts à partir du sixième, quel que soit le nombre total de calmars.

Bien que les applications en soient multiples, ce n’est pas en soi une méthode de sélection quantity-aware : nous ne stylons rien “parce qu’il y a six éléments ou plus au total”, nous ne faisons que styler les éléments qui se trouvent être après le cinquième élément.

Pour résoudre le problème de manière appropriée, ce dont nous avons vraiment besoin c’est de créer un ensemble d’éléments qui excluent les cinq derniers items. En utilisant l’opposé de :nth-child(n+6)—:nth — c’est à dire last-child(n+6) — Je peux appliquer les propriétés du layout modifié à tous les “derniers éléments” en commençant par le sixième, en comptant à rebours.

li:nth-last-child(n+6) {  
    /* vos propriétés ici */
}

De cette façon, on omet les cinq derniers items d’un ensemble de n’importe quelle taille, ce qui signifie que lorsqu’on réduit la longueur de cet ensemble en-deçà de six on ne voit plus aucun item sélectionné. C’est une sorte d’effet de “portes coulissantes”.

Un ensemble de calmars verts (à gauche) et rouges (à droite) devient un ensemble de calmars seulement rouges lorsque le nombre d’éléments est inférieur à six.

Mais si l’ensemble comporte six items ou plus, tout ce qu’il nous reste à faire est d’appliquer un style à ces cinq derniers items. C’est facile : lorsqu’il y a plus de six items, un ou plus d’items qui “retournent True” (pour parler en langage de programmation) pour la condition nth-last-child(n+6) doivent exister. Tout élément répondant à cette condition peut être combiné avec ~ (tilde) pour affecter tous les items (dont les cinq derniers) qui le suivent.

Quand un ensemble de calmars rouges se voit ajouter d’autres calmars, les calmars à droite deviennent verts et peuvent être utilisés pour rendre verts les autres calmars rouges (grâce au combinateur de voisins généraux)

La solution étonnamment sobre à notre problème est :

li:nth-last-child(n+6),  
li:nth-last-child(n+6) ~ li {  
    /* vos propriétés ici */
}

Bien entendu, on peut remplacer 6 par n’importe quel entier positif, même 653.279.

Inférieur ou égal à n

Tout comme dans notre exemple :only-of-type précédent, on peut inverser la logique pour passer de “supérieur ou égal à n” à “inférieur ou égal à n”. À vous de choisir la logique que vous préférez, selon l’état que vous considérez le plus naturel par défaut. “Inférieur ou égal à n” est réalisé en donnant une valeur négative à n et en réintégrant la condition :first-child.

li:nth-last-child(-n+6):first-child,  
li:nth-last-child(-n+6):first-child ~ li {  
    /* vos propriétés ici */
}

L’utilisation du signe “−” (moins) change la direction de la sélection : au lieu de pointer vers le début à partir du sixième, elle pointe vers la fin à partir du sixième. Dans tous les cas, le sélecteur comprend le sixième item.

nth-child versus nth-of-type
Vous avez remarqué que j’utilise :nth-child() et :nth-last-child() dans mes exemples et non :nth-of-type() ou :nth-last-of-type(), mais du fait que je m’occupe d’éléments <li> et que les <li> sont les seuls enfants légitimes de <ul>, :last-child() et :last-of-type() fonctionneraient tous les deux.

Les familles de sélecteurs :nth-child() et :nth-of-type() ont des avantages propres, donc tout dépend de ce que vous cherchez à réaliser. ( NdT : Pour une discussion plus approfondie sur le sujet, voyez les articles :nth-child et :nth-of-type du Dico CSS.). Comme :nth-child() est indépendant de l’élément, vous pouvez appliquer la technique à différents éléments frères :

<div class="container">  
    <p>...</p>
    <p>...</p>
    <blockquote>...</blockquote>
    <figure>...</figure>
    <p>...</p>
    <p>...</p>
</div>  

Le CSS :

.container > :nth-last-child(n+3),
.container > :nth-last-child(n+3) ~ * {
    /* vos propriétés ici */
}

(Remarquez que j’utilise le sélecteur universel pour conserver l’indépendance relativement aux éléments : :last-child(n+3) ~ * signifie “tout élément de n’importe quel type, qui suit :last-child(n+3)).

L’avantage de :nth-last-of-type() par contre est que vous pouvez cibler des groupes d’éléments semblables lorsque d’autres éléments frères de type différent sont présents. Par exemple, vous pourriez cibler la quantité de paragraphes dans le code qui suit, bien qu’ils soient précédés et suivis de <div> et <blockquote>.

<div class="container">  
    <div>...</div>
    <p>...</p>
    <p>...</p>
    <p>...</p>
    <p>...</p>
    <p>...</p>
    <p>...</p>
    <p>...</p>
    <blockquote>...</blockquote>
</div>  

Le CSS :

p:nth-last-of-type(n+6),  
p:nth-last-of-type(n+6) ~ p {  
    /* properties here */
}

Compatibilité des sélecteurs

Tous les sélecteurs CSS2.1 et CSS3 utilisés dans cet article sont supportés par IE9 et suivants, et par les navigateurs mobiles raisonnablement récents.

La compatibilité IE8 est bonne pour la plupart des types de sélecteurs mais techniquement partielle, il est donc sans doute souhaitable d’utiliser un polyfill JavaScript.

Dans le monde réel

Supposons que notre menu de navigation fasse partie d’un CMS. Selon la personne qui administre le thème, il serait rempli avec un nombre d’options plus ou moins grand. Certains auteurs feront simple, avec juste des liens “Page d’accueil” et “À propos”, d’autres surchargeront leur menu de pages personnalisées et d’options.

En fournissant des layouts alternatifs basés sur le nombre d’items de menu, nous améliorons l’élégance des diverses implémentations du thème et nous résolvons le problème du contenu variable de la même façon que nous sommes habitués à le faire pour les dimensions d’écran variables.

Comparaison du layout initial pour moins de 6 items avec le layout pour 6 items ou plus.

Et voilà le résultat : calmars à l’horizon ! Vous pouvez maintenant ajouter à votre répertoire la quantité comme une condition d’application de styles.

Design indépendant du contenu

Le design responsif résout un problème important : il permet à un même contenu d’être confortablement digéré par des terminaux différents. Il serait inacceptable pour les utilisateur de recevoir des contenus différents simplement parce que leurs terminaux ne seraient pas les mêmes. De façon similaire, on ne peut pas accepter qu’un design dicte la nature du contenu. Nous n’imaginerions pas de dire à un rédacteur “laisse tomber cette partie, tu veux bien ? ça me bousille mon design”.

Mais la forme que prendra le contenu, et sa quantité à un moment donné, sont souvent inconnus. Nous ne pouvons pas toujours compter sur les retours à la ligne et les scripts permettant de tronquer un texte. Pour vraiment maîtriser l’indépendance du contenu, nous avons besoin de développer de nouveaux outils et techniques. Les quantity queries sont simplement une idée dans ce sens.

Dans le design web tout est changement, différence, incertitude. On ne sait jamais. De manière unique, c’est une modalité du design visuel dans laquelle on ne manifeste pas une forme mais on anticipe les différentes formes qu’une chose pourrait prendre. Cela plonge certains dans une grande perplexité, mais pour vous et moi c’est un défi à savourer. Comme l’insaisissable calmar, c’est un client sérieusement glissant.


Intéressé par CSS ? Retrouvez une liste des meilleurs articles et ressources du web.

Tous les articles sur CSS parus dans la Cascade.

Le Dico CSS de la Cascade (work in progress).

Articles sur les mêmes thèmes dans la Cascade :

Des sélecteurs virtuels en CSS


Article original paru le 3 mars 2015 dans A List Apart.

Sur l’auteur : est designer à Norwich, UK. Il est lead designer à Neontribe et en charge de l’accessibilité pour Smashing Magazine, où son livre Apps For All est disponible. On peut le suivre sur Twitter et sur son site HeydonWorks.

Traduit avec l’aimable autorisation de A List Apart et de l’auteur.
Copyright A List Apart © 2015.