Vous avez déjà vécu ça, pas vrai ?
L’amélioration progressive
Quand on cherche à bien faire ces interactions qui affichent et masquent un élément à l’aide de JavaScript, une des fondations est de s’assurer que le contenu soit accessible sans JavaScript. Ça fait partie intégrante de l’amélioration progressive, que je synthétise comme suit :
- le HTML est propre, lisible, cohérent, et permet d’accéder au contenu sans obstacle ;
- le CSS améliore l’aspect graphique, ordonne les éléments visuellement ;
- le JS enrichit le tout d’une couche de comportements inexistants en HTML et CSS.
Or dans le cas d’un composant qui affiche ou masque un élément, le fonctionnement avant l’exécution du JS est donc d’avoir cet élément affiché par défaut. Ensuite — et seulement une fois que le JS est fonctionnel — on va pouvoir le manipuler pour masquer notre élément.
C’est là où le bât blesse. Le temps que notre JS soit exécuté, nous voyons l’élément affiché (même si ça ne dure qu’une fraction de seconde).
La limite du JS
En l’occurrence, c’est son temps d’exécution — extrêmement dépendant de la machine et du navigateur de l’utilisateur.
Plusieurs pistes existent pour palier ce problème :
- on peut par exemple exécuter le plus tôt possible le test d’activation du JS, qui consiste à transformer une classe
no-js
posée sur la balise<html>
… Mais dans certains cas, ce la ne suffit pas ; - on peut également abdiquer : après tout, plus aucun référentiel n’exige d’alternative à JS ;
- on peut aussi se la jouer old school en dupliquant le contenu masqué dans une balise
<noscript>
— mais on ne se sent pas vraiment propre, après ça.
Comme le signale Lionel dans les commentaires, conserver la modification des classes dès le <head>
reste une étape incontournable pour optimiser ce mécanisme. Un exemple :
document.documentElement.classList.remove(’no-js’);
document.documentElement.classList.add(’js’);
Et en bonus, on peut réfléchir et utiliser CSS.
Les styles à la rescousse
La base du fonctionnement que je propose est l’astuce utilisée par Nicolas Hoffmann sur ses composants jQuery accessibles. Grosso modo, il effectue une transition sur max-height
pour la partie animée, et sur visibility
avec un délai pour masquer réellement le contenu.[1]
J’aime beaucoup cette technique, dont le seul inconvénient — à mon avis — est d’animer max-height
, ce qui nous oblige à utiliser un chiffre magique pour une hauteur maximum inatteignable.
L’état de base
Voici l’état de base de ma navigation :
[id="nav"] {
transform: translate3d(-100%, 0, 0);
transition:
transform 300ms ease-in 50ms,
visibility 0s linear 300ms;
visibility: hidden;
width: 18.75rem;
will-change: transform, visibility;
}
Elle est décalée vers la gauche de la totalité de sa largeur, afin de sortir de l’écran ; et est masquée.
Notez que nous avons un délai sur nos deux transitions. Pour le moment, seul celui sur visibility
est important, puisqu’il permet de faire coïncider le changement de visibilité avec la durée de la transformation.
L’ouverture grâce à JS
Là, c’est tout bête. Le JS ajoute une classe .is-opened
à la navigation, je m’en sers pour accrocher mes styles :
.is-opened {
transform: none;
transition-delay: 0ms;
visibility: visible;
}
Et pour ceux qui me connaissent, je désamorce une question : je n’utilise pas :not([aria-hidden])
, car cet attribut est ajouté via JS. Ainsi la navigation commencerait à apparaître au chargement, puis serait masquée après l’exécution du script — exactement le comportement qu’on cherche à corriger.
Et si JS est désactivé
C’est là qu’on rigole ! Lisez plutôt, je vous explique ensuite :
@keyframes no-js {
to {
transform: none;
transition-delay: 50ms, 0ms;
visibility: visible;
}
}
.no-js [id="nav"] {
animation: 300ms ease-in 300ms forwards no-js;
}
Dans un premier temps, je définis la règle @keyframes
pour mon animation. Son seul contenu est l’état final : pas de translation, l’élément est visible, et les délais de transition sont ajustés. En terme de support on abandonne donc IE9 et ses aïeux, ainsi qu’Opéra Mini.
Dans un second temps, j’applique cette animation sur la navigation lorsque la balise <html>
porte la classe .no-js
. C’est sa classe par défaut, qui n’est retirée que si JS est activé.
Pour éviter que l’animation ne se joue pendant le chargement de la page, je lui intime l’ordre de patienter 300 millisecondes et de durer 300 millisecondes — soit un délai généralement suffisant pour que JS ait magouillé les classes sur <html>
.
Et ceux qui ont déjà joué avec les animations le savent, elles reviennent par défaut à l’état initial (soit dans notre cas, le menu masqué). Bien sûr, ça ne va pas : j’ai donc précisé grâce au mot-clé forwards
que l’animation devait conserver son état final.
Et voilà ! On profite en sus d’une bien jolie animation qui fait entrer notre navigation dans l’écran au lieu de la fuir. Et une page animée avec JS désactivé, je trouve ça cool. 🙂
Démonstration
Si vous êtes curieux de voir ce que ça donne ou de jouer avec, j’ai monté un CodePen de démonstration dans lequel vous pourrez lire le code.
Pour jouer avec le JS désactivé, vous devrez consulter la vue complète.[2]
Pour le JS qui prend du temps, pourquoi ne pas utiliser un script dans le
head
, qui va juste modifier laclass
? Ça évite d’avoir a utiliser un durée d’animation qui ne corrigera pas le problème sur les connexion lentes (j’ai testé en utilisant le network throttling de Chrome : on voit le menu à partir d’une connexion 3g).document.documentElement.classList.remove(’no-js’).add(’js’)
Et bien en fait c’est ma première proposition, dans le paragraphe « La limite du JS » 🙂
Comme je l’explique, ça ne fait que décaler le problème — et je cherchais avant tout une solution alternative. Cependant déclencher ce changement de classe depuis
head
en plus de l’animation permet de réduire les cas « gênants » comme celui que tu évoques, la faible connexion.Je comprend pas en quoi ça décale le problème, j’ai plutôt l’impression que ça le résous et de manière plus robuste que d’avoir recours à une durée d’animation arbitraire qui ne prend pas en compte la connexion / la puissance du device.
De plus ca répond à ton postulat de départ :
Le document n’a peut-être pas fini d’être parsé, le js n’a peut être même pas commencé à être téléchargé, la classe js sera déjà en place.
Et pourtant il y a vraisemblablement des conditions dans lesquelles ce délai extrêmement rapide ne suffit pas : c’est la solution que j’utilise depuis des lustres — à l’époque où l’on passait encore par une expression régulière sur
.className
— et ce problème existait…Par ailleurs pour jouer avec
.classList
, il semble que chaîner.add()
et.remove()
ne fonctionne pas : il faut les séparer.Quant à l’astuce de l’animation, elle reste pertinente dans la mesure où la distinction reste de toute façon plus évidente en partant de l’état masqué. Elle a également le mérite de dispenser un peu d’animation (et donc une potentielle scénarisation et mise en scène) du site quand JS est désactivé. 🙂