Construire un slider pleine page personnalisé avec CSS et JavaScript
Publié: 2022-03-11Je travaille beaucoup avec des mises en page personnalisées en plein écran, pratiquement au quotidien. Habituellement, ces mises en page impliquent une quantité substantielle d'interaction et d'animation. Qu'il s'agisse d'une chronologie complexe de transitions déclenchées par le temps ou d'un ensemble d'événements pilotés par l'utilisateur basé sur le défilement, dans la plupart des cas, l'interface utilisateur nécessite plus que la simple utilisation d'une solution de plug-in prête à l'emploi avec quelques ajustements et modifications. . D'un autre côté, je vois que de nombreux développeurs JavaScript ont tendance à rechercher leur plugin JS préféré pour faciliter leur travail, même si la tâche n'a peut-être pas besoin de toutes les cloches et sifflets qu'un certain plugin fournit.
Avis de non-responsabilité : L'utilisation de l'un des nombreux plugins disponibles a ses avantages, bien sûr. Vous obtiendrez une variété d'options que vous pourrez utiliser pour répondre à vos besoins sans avoir à faire beaucoup de codage. En outre, la plupart des auteurs de plugins optimisent leur code, le rendent compatible avec plusieurs navigateurs et plusieurs plates-formes, etc. Mais quand même, vous obtenez une bibliothèque pleine grandeur incluse dans votre projet pour peut-être seulement une ou deux choses différentes qu'elle fournit. Je ne dis pas qu'utiliser un plugin tiers quel qu'il soit est naturellement une mauvaise chose, je le fais quotidiennement dans mes projets, juste que c'est généralement une bonne idée de peser le pour et le contre de chaque approche telle qu'elle est une bonne pratique de codage. Quand il s'agit de faire votre propre chose de cette façon, cela nécessite un peu plus de connaissances et d'expérience en matière de codage pour savoir ce que vous recherchez, mais à la fin, vous devriez obtenir un morceau de code qui fait une chose et une seule chose de la manière vous le voulez.
Cet article vise à montrer une approche CSS/JS pure dans le développement d'une disposition de curseur déclenchée par défilement plein écran avec une animation de contenu personnalisée. Dans cette approche réduite, je couvrirai la structure HTML de base que vous attendez d'un back-end CMS, les techniques de mise en page CSS modernes (SCSS) et le codage JavaScript vanille pour une interactivité complète. Étant simple, ce concept peut être facilement étendu à un plugin à plus grande échelle et/ou utilisé dans une variété d'applications n'ayant aucune dépendance à la base.
Le design que nous allons créer est une vitrine de portefeuille d'architecte minimaliste avec des images et des titres en vedette de chaque projet. Le slider complet avec animations ressemblera à ceci :
Vous pouvez consulter la démo ici, et vous pouvez accéder à mon référentiel Github pour plus de détails.
Présentation HTML
Voici donc le code HTML de base avec lequel nous allons travailler :
<div> <div class="mask"> <!-- Textual logo will go here --> </div> <div> <div class="slides"> <!-- Featured image slides will go here --> </div> <div class="slides mask"> <!-- Slide titles will go here --> </div> </div> <div> <!-- Static info on the right --> </div> <nav> <!-- Current slide indicator --> </nav> </div>
Un div avec l'id de hero-slider
est notre support principal. À l'intérieur, la mise en page est divisée en sections :
- Logo (une section statique)
- Diaporama sur lequel nous travaillerons principalement
- Info (une section statique)
- Slider nav qui indiquera la diapositive actuellement active ainsi que le nombre total de diapositives
Concentrons-nous sur la section diaporama puisque c'est notre point d'intérêt dans cet article. Ici, nous avons deux parties : main et aux . Main est la div qui contient les images en vedette tandis que aux contient les titres des images. La structure de chaque diapositive à l'intérieur de ces deux supports est assez basique. Ici, nous avons une diapositive d'image à l'intérieur du support principal :
<div class="slide" data-index="0"> <div class="abs-mask"> <div class="slide-image"> </div> </div> </div>
L'attribut de données d'index est ce que nous utiliserons pour savoir où nous en sommes dans le diaporama. La div abs-mask que nous utiliserons pour créer un effet de transition intéressant et la div slide-image contient l'image sélectionnée spécifique. Les images sont rendues en ligne comme si elles provenaient directement d'un CMS et sont définies par l'utilisateur final.
De même, le titre glisse à l'intérieur du support auxiliaire :
<h2 class="slide-title slide" data-index="0"><a href="#">#64 Paradigm</a></h2>
Chaque titre de diapositive est une balise H2 avec l'attribut de données correspondant et un lien pour pouvoir mener à la page unique de ce projet.
Le reste de notre code HTML est également assez simple. Nous avons un logo en haut, des informations statiques qui indiquent à l'utilisateur sur quelle page il se trouve, une description et un indicateur actuel/total du curseur.
Présentation CSS
Le code CSS source est écrit en SCSS, un pré-processeur CSS qui est ensuite compilé en CSS normal que le navigateur peut interpréter. SCSS vous donne l'avantage d'utiliser des variables, une sélection imbriquée, des mixins et d'autres trucs sympas, mais il doit être compilé en CSS pour que le navigateur lise le code comme il se doit. Pour les besoins de ce tutoriel, j'ai utilisé Scout-App pour gérer la compilation car je voulais avoir l'outillage au strict minimum.
J'ai utilisé flexbox pour gérer la mise en page côte à côte de base. L'idée est d'avoir le diaporama d'un côté et la section d'information de l'autre.
#hero-slider { position: relative; height: 100vh; display: flex; background: $dark-color; } #slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #info { position: relative; flex: 1 1 $side-width; padding: $offset; background-color: #fff; }
Plongeons-nous dans le positionnement et encore une fois, concentrons-nous sur la section diaporama :
#slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #slides-main { @extend %abs; &:after { content: ''; @extend %abs; background-color: rgba(0, 0, 0, .25); z-index: 100; } .slide-image { @extend %abs; background-position: center; background-size: cover; z-index: -1; } } #slides-aux { position: relative; top: 1.25rem; width: 100%; .slide-title { position: absolute; z-index: 300; font-size: 4vw; font-weight: 700; line-height: 1.3; @include outlined(#fff); } }
J'ai défini le curseur principal sur une position absolue et les images d'arrière-plan étirent toute la zone en utilisant la propriété background-size: cover
. Pour fournir plus de contraste avec les titres des diapositives, j'ai défini un pseudo-élément absolu qui agit comme une superposition. Le curseur auxiliaire contenant les titres des diapositives est positionné en bas de l'écran et au-dessus des images.
Étant donné qu'une seule diapositive sera visible à la fois, je définis également chaque titre comme absolu et calcule la taille du support via JS pour s'assurer qu'il n'y a pas de coupures, mais plus à ce sujet dans l'une de nos prochaines sections. Ici, vous pouvez voir l'utilisation d'une fonctionnalité SCSS appelée extension :
%abs { position: absolute; top: 0; left: 0; height: 100%; width: 100%; }
Comme j'ai beaucoup utilisé le positionnement absolu, j'ai inséré ce CSS dans une extension pour qu'il soit facilement disponible dans divers sélecteurs. De plus, j'ai créé un mixin appelé "contourné" pour fournir une approche DRY lors du style des titres et du titre du curseur principal.
@mixin outlined($color: $dark-color, $size: 1px) { color: transparent; -webkit-text-stroke: $size $color; }
Quant à la partie statique de cette mise en page, elle n'a rien de complexe mais ici vous pouvez voir une méthode intéressante lors du positionnement du texte qui doit être sur l'axe Y au lieu de son flux normal :
.slider-title-wrapper { position: absolute; top: $offset; left: calc(100% - #{$offset}); transform-origin: 0% 0%; transform: rotate(90deg); @include outlined; }
J'aimerais attirer votre attention sur la propriété transform-origin
car je l'ai trouvée vraiment sous-utilisée pour ce type de mise en page. La façon dont cet élément est positionné est que son ancre reste dans le coin supérieur gauche de l'élément, définissant le point de rotation et faisant couler le texte en continu à partir de ce point vers le bas sans aucun problème lorsqu'il s'agit de différentes tailles d'écran.
Jetons un coup d'œil à une partie CSS plus intéressante - l'animation de chargement initiale :
Habituellement, ce type de comportement d'animation synchronisé est obtenu à l'aide d'une bibliothèque - GSAP, par exemple, est l'un des meilleurs du marché, offrant d'excellentes capacités de rendu, est facile à utiliser et possède la fonctionnalité de chronologie qui permet au développeur de chaîner des éléments par programmation. transitions les unes dans les autres.
Cependant, comme il s'agit d'un pur exemple CSS/JS, j'ai décidé d'aller vraiment à la base ici. Ainsi, chaque élément est défini sur sa position de départ par défaut, soit masqué par transformation, soit par opacité et affiché lors du chargement du curseur déclenché par notre JS. Toutes les propriétés de transition sont modifiées manuellement pour assurer un flux naturel et intéressant, chaque transition se poursuivant dans une autre offrant une expérience visuelle agréable.
#logo:after { transform: scaleY(0); transform-origin: 50% 0; transition: transform .35s $easing; } .logo-text { display: block; transform: translate3d(120%, 0, 0); opacity: 0; transition: transform .8s .2s, opacity .5s .2s; } .current, .sep:before { opacity: 0; transition: opacity .4s 1.3s; } #info { transform: translate3d(100%, 0, 0); transition: transform 1s $easing .6s; } .line { transform-origin: 0% 0; transform: scaleX(0); transition: transform .7s $easing 1s; } .slider-title { overflow: hidden; >span { display: block; transform: translate3d(0, -100%, 0); transition: transform .5s 1.5s; } }
S'il y a une chose que j'aimerais que vous voyiez ici, c'est l'utilisation de la propriété transform
. Lors du déplacement d'un élément HTML, qu'il s'agisse d'une transition ou d'une animation, il est conseillé d'utiliser la propriété transform
. Je vois beaucoup de gens qui ont tendance à ruser soit sur la marge, soit sur le rembourrage, soit même sur les décalages - en haut, à gauche, etc., ce qui ne produit pas de résultats adéquats en matière de rendu.
Pour acquérir une compréhension plus approfondie de la façon d'utiliser CSS lors de l'ajout d'un comportement interactif, je ne saurais trop recommander l'article suivant.
C'est par Paul Lewis, un ingénieur Chrome, et couvre à peu près tout ce que l'on doit savoir sur le rendu des pixels sur le Web, que ce soit CSS ou JS.
Présentation de JavaScript et logique de curseur
Le fichier JavaScript est divisé en deux fonctions distinctes.
La fonction heroSlider
qui s'occupe de toutes les fonctionnalités dont nous avons besoin ici, et la fonction utils
où j'ai ajouté plusieurs fonctions utilitaires réutilisables. J'ai commenté chacune de ces fonctions utilitaires pour fournir un contexte si vous cherchez à les réutiliser dans votre projet.
La fonction principale est codée de manière à avoir deux branches : init
et resize
. Ces branches sont disponibles via le retour de la fonction main et sont appelées si nécessaire. init
est l'initialisation de la fonction principale et elle est déclenchée lors de l'événement de chargement de la fenêtre. De même, la branche de redimensionnement est déclenchée lors du redimensionnement de la fenêtre. Le seul but de la fonction de redimensionnement est de recalculer la taille du curseur du titre lors du redimensionnement de la fenêtre, car la taille de la police du titre peut varier.
Dans la fonction heroSlider
, j'ai fourni un objet slider qui contient toutes les données et les sélecteurs dont nous aurons besoin :
const slider = { hero: document.querySelector('#hero-slider'), main: document.querySelector('#slides-main'), aux: document.querySelector('#slides-aux'), current: document.querySelector('#slider-nav .current'), handle: null, idle: true, activeIndex: -1, interval: 3500 };
En passant, cette approche pourrait être facilement adaptée si vous utilisez par exemple React, car vous pouvez stocker les données dans l'état ou utiliser les crochets nouvellement ajoutés. Pour rester sur le point, passons simplement en revue ce que chacune des paires clé-valeur représente ici :

- Les quatre premières propriétés sont une référence HTML à l'élément DOM que nous allons manipuler.
- La propriété
handle
sera utilisée pour démarrer et arrêter la fonctionnalité de lecture automatique. - La propriété
idle
est un indicateur qui empêchera l'utilisateur de forcer le défilement pendant que la diapositive est en transition. -
activeIndex
nous permettra de garder une trace de la diapositive actuellement active -
interval
indique l'intervalle de lecture automatique du curseur
Lors de l'initialisation du curseur, nous invoquons deux fonctions :
setHeight(slider.aux, slider.aux.querySelectorAll('.slide-title')); loadingAnimation();
La fonction setHeight
fait appel à une fonction utilitaire pour définir la hauteur de notre curseur auxiliaire en fonction de la taille maximale du titre. De cette façon, nous nous assurons que le dimensionnement adéquat est fourni et qu'aucun titre de diapositive ne sera coupé même lorsque son contenu tombe sur deux lignes.
La fonction loadingAnimation ajoute une classe CSS à l'élément fournissant les transitions CSS d'introduction :
const loadingAnimation = function () { slider.hero.classList.add('ready'); slider.current.addEventListener('transitionend', start, { once: true }); }
Comme notre indicateur de curseur est le dernier élément de la chronologie de transition CSS, nous attendons la fin de sa transition et invoquons la fonction de démarrage. En fournissant un paramètre supplémentaire en tant qu'objet, nous nous assurons que cela n'est déclenché qu'une seule fois.
Regardons la fonction start :
const start = function () { autoplay(true); wheelControl(); window.innerWidth <= 1024 && touchControl(); slider.aux.addEventListener('transitionend', loaded, { once: true }); }
Ainsi, lorsque la mise en page est terminée, sa transition initiale est déclenchée par la fonction loadingAnimation
et la fonction start prend le relais. Il déclenche ensuite la fonctionnalité de lecture automatique, active le contrôle de la molette, détermine si nous sommes sur un appareil tactile ou de bureau et attend que la première transition des titres glisse pour ajouter la classe CSS appropriée.
Lecture automatique
L'une des principales fonctionnalités de cette mise en page est la fonction de lecture automatique. Passons en revue la fonction correspondante :
const autoplay = function (initial) { slider.autoplay = true; slider.items = slider.hero.querySelectorAll('[data-index]'); slider.total = slider.items.length / 2; const loop = () => changeSlide('next'); initial && requestAnimationFrame(loop); slider.handle = utils().requestInterval(loop, slider.interval); }
Tout d'abord, nous définissons l'indicateur de lecture automatique sur vrai, indiquant que le curseur est en mode de lecture automatique. Cet indicateur est utile pour déterminer s'il faut relancer la lecture automatique après que l'utilisateur a interagi avec le curseur. Nous référençons ensuite tous les éléments de curseur (diapositives), car nous allons changer leur classe active et calculer le nombre total d'itérations que le curseur va avoir en additionnant tous les éléments et en divisant par deux car nous avons deux dispositions de curseur synchronisées (main et aux) mais un seul "curseur" en soi qui change les deux simultanément.
La partie la plus intéressante du code ici est la fonction de boucle. Il invoque slideChange
, fournissant la direction de la diapositive que nous verrons dans une minute, cependant, la fonction de boucle est appelée plusieurs fois. Voyons pourquoi.
Si l'argument initial est évalué comme vrai, nous invoquerons la fonction de boucle en tant que rappel de requestAnimationFrame
. Cela ne se produit qu'au premier chargement du curseur, ce qui déclenche un changement de diapositive immédiat. En utilisant requestAnimationFrame
nous exécutons le rappel fourni juste avant le prochain repaint du cadre.
Cependant, comme nous voulons continuer à parcourir les diapositives en mode de lecture automatique, nous utiliserons un appel répété de cette même fonction. Ceci est généralement réalisé avec setInterval. Mais dans ce cas, nous utiliserons l'une des fonctions utilitaires requestInterval
. Alors que setInterval
fonctionnerait très bien, requestInterval
est un concept avancé qui s'appuie sur requestAnimationFrame
et fournit une approche plus performante. Il garantit que la fonction n'est redéclenchée que si l'onglet du navigateur est actif.
Vous trouverez plus d'informations sur ce concept dans cet article génial sur les astuces CSS. Veuillez noter que nous attribuons la valeur de retour de cette fonction à notre propriété slider.handle
. Cet identifiant unique renvoyé par la fonction est disponible pour nous et nous l'utiliserons pour annuler la lecture automatique ultérieurement à l'aide de cancelAnimationFrame
.
Changement de diapositive
La fonction slideChange
est la fonction principale de tout le concept. Il change les diapositives, que ce soit par lecture automatique ou par déclencheur utilisateur. Il est conscient de la direction du curseur, fournit une boucle de sorte que lorsque vous arrivez à la dernière diapositive, vous pourrez continuer jusqu'à la première diapositive. Voici comment je l'ai codé:
const changeSlide = function (direction) { slider.idle = false; slider.hero.classList.remove('prev', 'next'); if (direction == 'next') { slider.activeIndex = (slider.activeIndex + 1) % slider.total; slider.hero.classList.add('next'); } else { slider.activeIndex = (slider.activeIndex - 1 + slider.total) % slider.total; slider.hero.classList.add('prev'); } //reset classes utils().removeClasses(slider.items, ['prev', 'active']); //set prev const prevItems = [...slider.items] .filter(item => { let prevIndex; if (slider.hero.classList.contains('prev')) { prevIndex = slider.activeIndex == slider.total - 1 ? 0 : slider.activeIndex + 1; } else { prevIndex = slider.activeIndex == 0 ? slider.total - 1 : slider.activeIndex - 1; } return item.dataset.index == prevIndex; }); //set active const activeItems = [...slider.items] .filter(item => { return item.dataset.index == slider.activeIndex; }); utils().addClasses(prevItems, ['prev']); utils().addClasses(activeItems, ['active']); setCurrent(); const activeImageItem = slider.main.querySelector('.active'); activeImageItem.addEventListener('transitionend', waitForIdle, { once: true }); }
L'idée est de déterminer la diapositive active en fonction de son index de données obtenu à partir de HTML. Abordons chaque étape :
- Définissez l'indicateur d'inactivité du curseur sur faux. Cela indique que le changement de diapositive est en cours et que les gestes de la molette et du toucher sont désactivés.
- La classe CSS de direction du curseur précédente est réinitialisée et nous vérifions la nouvelle. Le paramètre direction est fourni soit par défaut comme 'next' si nous venons de la fonction de lecture automatique ou par une fonction invoquée par l'utilisateur
wheelControl
outouchControl
. - En fonction de la direction, nous calculons l'index de la diapositive active et fournissons la classe CSS de direction actuelle au curseur. Cette classe CSS est utilisée pour déterminer quel effet de transition sera utilisé (par exemple de droite à gauche ou de gauche à droite)
- Les diapositives réinitialisent leurs classes CSS "d'état" (prev, active) à l'aide d'une autre fonction utilitaire qui supprime les classes CSS mais peut être invoquée sur une NodeList, plutôt que sur un seul élément DOM. Ensuite, seules les diapositives précédentes et actuellement actives reçoivent ces classes CSS qui leur sont ajoutées. Cela permet au CSS de cibler uniquement ces diapositives et de fournir une transition adéquate.
-
setCurrent
est un rappel qui met à jour l'indicateur de curseur en fonction de l'activeIndex. - Enfin, nous attendons que la transition de la diapositive d'image active se termine afin de déclencher le callback
waitForIdle
qui relance la lecture automatique si elle a été précédemment interrompue par l'utilisateur.
Commandes utilisateur
En fonction de la taille de l'écran, j'ai ajouté deux types de commandes utilisateur : la molette et le toucher. Commande au volant :
const wheelControl = function () { slider.hero.addEventListener('wheel', e => { if (slider.idle) { const direction = e.deltaY > 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } }); }
Ici, nous écoutons la roue même et si le curseur est actuellement en mode inactif (n'animant pas actuellement un changement de diapositive), nous déterminons la direction de la roue, stopAutoplay
pour arrêter la fonction de lecture automatique si elle est en cours et changeons la diapositive en fonction de la direction. La fonction stopAutoplay
n'est rien d'autre qu'une fonction simple qui définit notre indicateur de lecture automatique sur la valeur false et annule notre intervalle en appelant la fonction utilitaire cancelRequestInterval
en lui transmettant le handle approprié :
const stopAutoplay = function () { slider.autoplay = false; utils().clearRequestInterval(slider.handle); }
Semblable à wheelControl
, nous avons touchControl
qui s'occupe des gestes tactiles :
const touchControl = function () { const touchStart = function (e) { slider.ts = parseInt(e.changedTouches[0].clientX); window.scrollTop = 0; } const touchMove = function (e) { slider.tm = parseInt(e.changedTouches[0].clientX); const delta = slider.tm - slider.ts; window.scrollTop = 0; if (slider.idle) { const direction = delta < 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } } slider.hero.addEventListener('touchstart', touchStart); slider.hero.addEventListener('touchmove', touchMove); }
Nous écoutons deux événements : touchstart
et touchmove
. Ensuite, nous calculons la différence. S'il renvoie une valeur négative, nous passons à la diapositive suivante car l'utilisateur a glissé de droite à gauche. En revanche, si la valeur est positive, c'est-à-dire que l'utilisateur a balayé de gauche à droite, nous déclenchons slideChange
avec la direction passée comme "précédent". Dans les deux cas, la fonctionnalité de lecture automatique est arrêtée.
Il s'agit d'une implémentation de geste utilisateur assez simple. Pour tirer parti de cela, nous pourrions ajouter des boutons précédent/suivant pour déclencher slideChange
au clic ou ajouter une liste à puces pour accéder directement à une diapositive en fonction de son index.
Conclusion et réflexions finales sur CSS
Alors voilà, une manière pure CSS/JS de coder une disposition de curseur non standard avec des effets de transition modernes.
J'espère que vous trouverez cette approche utile comme mode de pensée et que vous pourrez utiliser quelque chose de similaire dans vos projets frontaux lors du codage d'un projet qui n'a pas nécessairement été conçu de manière conventionnelle.
Pour ceux d'entre vous qui s'intéressent à l'effet de transition d'image, j'y reviendrai dans les prochaines lignes.
Si nous revoyons la structure HTML des diapositives que j'ai fournie dans la section d'introduction, nous verrons que chaque diapositive d'image est entourée d'un div
avec la classe CSS de abs-mask
. Ce que fait cette div
, c'est qu'elle cache une partie de l'image visible d'une certaine quantité en utilisant overflow:hidden
et en la décalant dans une direction différente de celle de l'image. Par exemple, si nous regardons la façon dont la diapositive précédente est codée :
&.prev { z-index: 5; transform: translate3d(-100%, 0, 0); transition: 1s $easing; .abs-mask { transform: translateX(80%); transition: 1s $easing; } }
La diapositive précédente a un décalage de -100 % sur son axe X, ce qui la déplace vers la gauche de la diapositive actuelle, cependant, la div abs-mask
interne est déplacée de 80 % vers la droite, offrant une fenêtre d'affichage plus étroite. Ceci, combiné à un z-index plus grand pour la diapositive active, donne une sorte d'effet de couverture - l'image active couvre la précédente tout en étendant sa zone visible en déplaçant le masque qui offre une vue complète.