Comme promis dans l’article précédent, nous allons aujourd’hui approfondir un peu l’utilisation du sélecteur « ~ » avec un cas pratique. Aujourd’hui, nous allons apprendre à développer un carousel en CSS 100% fonctionnel ! Un projet qui cette fois sera vraiment utilisable facilement sur un site vitrine.

Le corps du projet

Pour commencer nous allons dans une page HTML simple créer une div qui servira de container principal à l’intérieur de laquelle nous aurons notre carousel. Appelons-la donc « slider » et notre component principal sera « testimonials » (vu que là c’est ça que ça représente, soyons fous).

<!DOCTYPE html>
<html lang="fr">
<head>
	<title>Un test</title>
	<meta charset="utf-8">
	<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
    <div class="slider">
        <!-- Des choses iront ici -->
        <div class="testimonials">
            <!-- Et le principal ici -->
        </div>
        <!-- Mais aussi 2-3 choses ici -->
    </div>
</body>
</html>

Si vous vous souvenez bien de l’article précédent, j’ai mon petit sélecteur favori, le ~ ! Avec ce sélecteur je peux récupérer un élément situé au même niveau q’un autre. Donc je peux agir avec un élément qui n’est pas à l’intérieur de l’élément que j’ai de base.

Pas clair ? Soyons plus précis ! Si je veux sélectionner un paragraphe dans une div, c’est très simple, je fais:

div p {
    color: #fff;
}
/* Ou bien encore */
div > p {
    color: #fff;
}

Jusque là tout va bien. Mais si je sélectionne un input qui a été coché, donc radio ou checkbox. Comment puis-je agir sur un autre élément ? Par définition ces balises étant auto-fermantes, elles ne peuvent pas contenir d’enfants. Pour cela nous avons deux sélecteurs, le + et le ~. Les deux vont avoir exactement le même mode de fonctionnement, à ceci près que le + ne sélectionnera ce que l’on veut que s’il est situé exactement en dessous de notre élément de base. Il est donc moins permissif (bien que très pratique dans certains cas), nous allons donc pouvoir faire:

<input type="radio">
<p>Hello World</p>
input:checked ~ p {
    color: green;
}

Si vous testez ce bout de code, votre paragraphe deviendra vert seulement une fois l’input sélectionné ! Car notre sélecteur demande un p situé après (mais au même niveau) qu’un input sélectionné. Nous pouvons donc avec ce mécanisme agir sur notre page en fonction de si un input est coché ou non !

Revenons à nous moutons d’inputs

Ici nous allons vouloir avoir deux comportements: bouger les cards quand une est sélectionnée, et faire apparaître son point (les petits points en dessous là) plus gros. Nous devons donc agir sur deux éléments différents. Le mieux pour ça est donc de placer nos inputs à la racine de notre élément, dans notre div slider. Et comme nous voulons cinq slides (si, si) et que nous ne voulons en afficher qu’une à la fois, nous passerons par cinq inputs radio avec le même name au dessus de notre div testimonials:

<input type="radio" name="testimonial" id="t-1">
<input type="radio" name="testimonial" id="t-2">
<input type="radio" name="testimonial" id="t-3" checked>
<input type="radio" name="testimonial" id="t-4">
<input type="radio" name="testimonial" id="t-5">
<!-- Ici la div testimonials -->

Notons que d’avance je met le 3e en coché par défaut. Il en faut bien un, et pour que ce soit plus joli, autant que ce soit celui du centre.

Comme nous voulons pouvoir cocher ces inputs en cliquant soit sur les points soit sur les cards, ces deux éléments seront des labels. Cela permet de trigger un input depuis n’importe quel endroit du code pour peu que l’attribut « for » de notre label soit le même que l’attribut « id » de notre input.

Les cards et les dots

Pour les cards, nous allons rester simple: un label qui contient une image, un paragraphe et un titre (h2 ici, normalement vous avez déjà un h1 quelque part). Je vous met ici la structure de la card 1, je vous laisse ajouter les autres en incrémentant le « for=’t-1′ » vous-même:

<label class="item" for="t-1">
	<img src="https://dummyimage.com/150" alt="picture">
	<p>"Raw denim you probably haven't heard of them jean short austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse."</p>
	<h2>- Princy, Web Developer</h2>
</label>

Et pour les points, nous allons créer une div « dots » et ajouter cinq labels dedans avec juste le for associé, et le CSS fera la suite !

<div class="dots">
	<label for="t-1"></label>
	<label for="t-2"></label>
	<label for="t-3"></label>
	<label for="t-4"></label>
	<label for="t-5"></label>
</div>

Et voilà, notre HTML est prêt. Nous n’avons plus qu’à mettre un peu tout ça en forme !

Notre carousel sans CSS

Stylisons notre Carousel !

Blablabla coutume blabla, commençons par un joli body tout propre ! Petit background dégradé, texte en blanc, le flex pour tout aligner, la police d’écriture et une min height. Rien d’extravagant, juste le nécessaire:

body {
	margin: 0;
	background-image: linear-gradient(90deg, #201A35, #0B0320);
	color: #fff;
	font-family: sans-serif;
	display: flex;
	align-items: center;
	min-height: 100vh;
}

Ensuite nous allons simplement mettre notre slider à 100% de width et cacher nos inputs afin de pouvoir ensuite nous concentrer sur l’essentiel:

.slider {
	width: 100%;
}
.slider input {
	display: none;
}

Et pour notre component principal, quelques petites subtilités. Déjà on passe tout en flex et on centre sur les deux axes. Ensuite vu que (spoilers !) nous allons utiliser de l’absolute (ça évite de gérer le flux des items entre eux, croyez-moi c’est plus simple), on met d’avance une position: relative. On met une min-height à 350px en cas de resize et un overflow:hidden pour éviter les scroll horizontaux quand les items sortent du cadre (ce qui va surtout arriver sur les petits devices, mais le projet est plus ou moins supposé dépasser).

Et la petite nouveauté… On va ajouter 1000px de perspective. Si, si ! Vous verrez plus tard son effet, mais mettons-le dès maintenant pour ne pas avoir à y revenir !

.testimonials {
	display: flex;
	align-items: center;
	justify-content: center;
	position: relative;
	min-height: 350px;
	perspective: 1000px;
	overflow: hidden;
}

Le style des cards

On va commencer par la partie simple ! Ajoutons déjà une width (la height sera auto en fonction du contenu), un petit padding, un border radius et une jolie couleur de fond. On en a parlé tout à l’heure, on va aussi ajouter une position: absolute, et on met le top à 0 pour que les éléments se collent bien en haut et n’empiètent pas sur nos dots. Histoire que la width ne s’additionne pas à notre padding, on met un box-sizing en border-box, et on vient centrer tout le texte. Une petite ombre pour bien distinguer les cartes entre elles (vu qu’elles se superposent) et un cursor: pointer. Ici un des soucis est que souvent en interagissant avec notre carousel, les utilisateurs peuvent sans le vouloir sélectionner le texte. Et c’est laid, et pas ce qu’on veut. Donc on rajoute un petit user-select: none. Et histoire que nos cards ne se téléportent pas, donnant l’air de bugger, on ajoute une petite transition-duration qui va donner un côté smooth à tout ça ! Facile ? Facile !

.testimonials .item {
	width: 450px;
	padding: 30px;
	border-radius: 5px;
	background-color: #0A0220;
	position: absolute;
	top: 0;
	box-sizing: border-box;
	text-align: center;
	transition: transform 0.4s;
	box-shadow: 0 0 10px rgba(0,0,0,0.3);
	user-select: none;
	cursor: pointer;
}

Maintenant trois petites choses. Déjà l’image, on va l’arrondir, lui donner une taille et ajouter une border. Ensuite on grise très légèrement notre paragraphe de façon à donner un poil plus d’importance au h2. Puis on vient augmenter légèrement la taille du h2.

Petit tips pour l’image. Pour être sûrs d’avoir des ronds en toute circonstance on peut mettre la height ET la width. Ça vous le savez. Mais là le soucis est qu’on se retrouve parfois avec des déformations si l’image n’a pas un ratio de 1:1. On peut alors mettre un object-fit: cover afin de forcer l’image à couvrir toute la surface de l’élément image mais sans déformation, et donc en cachant ce qui est de trop:

.testimonials .item img {
	width: 100px;
        height: 100px;
	object-fit: cover;
	border-radius: 50%;
	border: 13px solid #3B344D;
}
.testimonials .item p {
	color: #ddd;
}
.testimonials .item h2 {
	font-size: 14px;
}

Des petits points, encore des petits points

Bien que pas idispensables, les points ajoutent un plus UX à notre carousel. On va cependant faire simple. Pour le conteneur, on passe simplement en flex et on centre tout. Pour les points, on leur donne une height, une width, un radius (on fait un rond quoi), et on ajoute un cursor: pointer pour indiquer l’interactivité. On change un poil le background en le mettant plus clair, seul le point sélectionné sera blanc. On ajoute un peu de marge pour les espacer, et on met une petite transition-duration pour l’effet smooth.

.dots {
	display: flex;
	justify-content: center;
	align-items: center;
}
.dots label {
	height: 5px;
	width: 5px;
	border-radius: 50%;
	cursor: pointer;
	background-color: #413B52;
	margin: 7px;
	transition-duration: 0.2s;
}
Le début de notre style
Si vous ne voyez pas toutes les cards c’est normal, elles sont superposées

Faire marcher tout ça

On va commencer par le plus simple: les points. On veut faire deux actions ici:

  • Si un item est sélectionné, son point est blanc et un peu plus gros
  • Si un item est sélectionné, les deux points autour sont un poil plus gros

À deux exceptions près: les points 1 et 5 n’auront qu’un seul voisin agrandi si ils sont sélectionnés vu que l’on a ni 0 ni 6. Pour ça, on va passer par le comportement décrit plus haut avec le ~. On va chercher à sélectionner un point situé dans notre div .dots qui est au même niveau que notre input checké. Et ça se traduit comme ça:

#t-1:checked ~ .dots label[for="t-1"]{
	transform: scale(2);
	background-color: #fff;
}

Si notre input #t-1 est checked, on récupère le label qui est dans notre .dots (qui lui est frère de #t-1). On doit passer par « .dots » vu que les labels ne sont pas frères de #t-1. Une fois cela fait, on peut dupliquer notre sélecteur pour avoir tous les cas de figures visés:

#t-1:checked ~ .dots label[for="t-1"],
#t-2:checked ~ .dots label[for="t-2"],
#t-3:checked ~ .dots label[for="t-3"],
#t-4:checked ~ .dots label[for="t-4"],
#t-5:checked ~ .dots label[for="t-5"] {
	transform: scale(2);
	background-color: #fff;
}

Comme ça, si vous cliquez sur un point, il devient blanc.

Maintenant si mon #t-3 est sélectionné, je veux que les labels 2 et 4 soient grandis. Si c’est #t-4 je veux que 3 et 5 soient grandis, et ainsi de suite… Pour cela, même logique:

#t-3:checked ~ .dots label[for="t-2"],
#t-3:checked ~ .dots label[for="t-4"] {
	transform: scale(1.5);
}

Et là encore, on duplique le sélecteur pour tous les cas souhaités:

#t-1:checked ~ .dots label[for="t-2"],
#t-2:checked ~ .dots label[for="t-1"],
#t-2:checked ~ .dots label[for="t-3"],
#t-3:checked ~ .dots label[for="t-2"],
#t-3:checked ~ .dots label[for="t-4"],
#t-4:checked ~ .dots label[for="t-3"],
#t-4:checked ~ .dots label[for="t-5"],
#t-5:checked ~ .dots label[for="t-4"] {
	transform: scale(1.5);
}

Et le plus gros, les cards de notre carousel

Pour les cards la logique est sensiblement la même, la question n’est donc pas de savoir comment faire le sélecteur, mais ce que l’on veut mettre dedans.

Ici la position par défaut (donc celle que l’on a actuellement), c’est celle d’un item sélectionné. On doit donc définir la position des items -1, -2, +1 et +2. Bien sûr en prenant en compte que les index 1 et 5 n’ont qu’un seul voisin. Donc quand #t-3 est sélectionné, je veux décaler le label t-2:

#t-3:checked ~ .testimonials label[for="t-2"] {
    transform: translate3d(-300px, 0, -90px) rotateY(15deg);
}

Ici je vous épargne l’explication du translate3d, souvenez-vous juste que l’ordre reste x, y puis z. En revanche c’est le rotateY qui nous intéresse. Vous vous souvenez du perspective: 1000px de tout à l’heure ? Retirez-le ? Maintenant remplacez-le par 100px ? Voilà, vous voyez maintenant un peu mieux le pourquoi du comment, revenons à nos slides. Pour la slide 4 on fait pareil:

#t-3:checked ~ .testimonials label[for="t-4"] {
    transform: translate3d(300px, 0, -90px) rotateY(-15deg);
}

Vous noterez qu’à part le Z qui reste identique, on vient juste inverser ! Maintenant on garde la même logique pour les deux autres, 1 et 5:

#t-3:checked ~ .testimonials label[for="t-1"] {
	transform: translate3d(-600px, 0, -180px) rotateY(25deg);
}
#t-3:checked ~ .testimonials label[for="t-5"] {
	transform: translate3d(600px, 0, -180px) rotateY(-25deg);
}

Vous remarquez sûrement un petit bug. Les cards se superposent les unes aux autres. On va donc devoir gérer les z-index. Comme on peut avoir jusqu’à 4 couches (si 1 ou 5 sont sélectionnées), notre label sélectionné sera en 4, les +1/-1 en 3, les +2/-2 en 2 et les -3/+3 en 1.

Rendons ça générique

Bon, et là arrive la partie où vous devez faire tous les cas de figures dans tous les sens, vu que vous avez compris la logique je vous la donne en cadeau:

#t-1:checked ~ .testimonials label[for="t-5"] {
	transform: translate3d(1200px, 0, -360px) rotateY(-45deg);
}
#t-1:checked ~ .testimonials label[for="t-4"],
#t-2:checked ~ .testimonials label[for="t-5"] {
	transform: translate3d(900px, 0, -270px) rotateY(-35deg);
	z-index: 1;
}
#t-1:checked ~ .testimonials label[for="t-3"],
#t-2:checked ~ .testimonials label[for="t-4"],
#t-3:checked ~ .testimonials label[for="t-5"] {
	transform: translate3d(600px, 0, -180px) rotateY(-25deg);
	z-index: 2;
}
#t-1:checked ~ .testimonials label[for="t-2"],
#t-2:checked ~ .testimonials label[for="t-3"],
#t-3:checked ~ .testimonials label[for="t-4"],
#t-4:checked ~ .testimonials label[for="t-5"] {
	transform: translate3d(300px, 0, -90px) rotateY(-15deg);
	z-index: 3;
}
#t-2:checked ~ .testimonials label[for="t-1"],
#t-3:checked ~ .testimonials label[for="t-2"],
#t-4:checked ~ .testimonials label[for="t-3"],
#t-5:checked ~ .testimonials label[for="t-4"] {
	transform: translate3d(-300px, 0, -90px) rotateY(15deg);
	z-index: 3;
}
#t-3:checked ~ .testimonials label[for="t-1"],
#t-4:checked ~ .testimonials label[for="t-2"],
#t-5:checked ~ .testimonials label[for="t-3"] {
	transform: translate3d(-600px, 0, -180px) rotateY(25deg);
}
#t-5:checked ~ .testimonials label[for="t-2"],
#t-4:checked ~ .testimonials label[for="t-1"] {
	transform: translate3d(-900px, 0, -270px) rotateY(35deg);
}
#t-5:checked ~ .testimonials label[for="t-1"] {
	transform: translate3d(-1200px, 0, -360px) rotateY(45deg);
}

Et on n’oublie juste pas de mettre les items sélectionnés en z-index: 4 pour qu’ils apparaissent toujours en début de pile:

#t-1:checked ~ .testimonials label[for="t-1"],
#t-2:checked ~ .testimonials label[for="t-2"],
#t-3:checked ~ .testimonials label[for="t-3"],
#t-4:checked ~ .testimonials label[for="t-4"],
#t-5:checked ~ .testimonials label[for="t-4"],
#t-5:checked ~ .testimonials label[for="t-5"] {
	z-index: 4;
}

Et voilà ! Vous avez réalisé un carousel 100% CSS en à peine 130 lignes !

Il est évidemment loin d’être parfait, mais ça vous donne déjà une bonne base de travail pour l’intégrer à un projet ! Maintenant à vous de jouer. Et si vous tentiez de créer l’illusion qu’il est infini ? (Oui c’est faisable)

Je vous laisse le projet entier via ce codepen, et on se retrouve bientôt pour de nouveaux articles sur ces aspects étonnants du CSS !