{"id":5491,"date":"2015-11-12T08:43:22","date_gmt":"2015-11-12T07:43:22","guid":{"rendered":"https:\/\/www.creativejuiz.fr\/blog\/?p=5491"},"modified":"2017-01-26T12:55:07","modified_gmt":"2017-01-26T11:55:07","slug":"creer-menu-sticky-avec-javascript-css","status":"publish","type":"post","link":"https:\/\/www.creativejuiz.fr\/blog\/tutoriels\/creer-menu-sticky-avec-javascript-css","title":{"rendered":"Cr\u00e9er un menu sticky avec JavaScript et CSS"},"content":{"rendered":"<p>J&rsquo;ai boss\u00e9 r\u00e9cemment sur plusieurs sites web, et la requ\u00eate d&rsquo;un menu <em>sticky<\/em> \u00e9tait quasiment syst\u00e9matique. Parfois elle \u00e9tait justifi\u00e9e, parfois je me rapprochais du contre-exemple ergonomique st\u00e9r\u00e9otyp\u00e9. (<a href=\"http:\/\/doisjeutiliser.fr\/unHeaderFooterFixes\/#pourquoi-utiliser\">Dois-je utiliser un <em>sticky<\/em> menu ?<\/a>) Mais quand m\u00eame ! Je vous propose de voir ensemble comment on peut faire \u00e7a.<\/p>\n<p><!--more--><\/p>\n<h2>Concept du menu <em>sticky<\/em><\/h2>\n<p>J&rsquo;aime bien partir d&rsquo;une id\u00e9e et poser quelques \u00e9l\u00e9ments par \u00e9crit pour ne pas partir t\u00eate baiss\u00e9e dans le code. Dans un premier temps, nous sommes tous d&rsquo;accord sur la d\u00e9finition du <strong>menu <em>sticky<\/em><\/strong> ? C&rsquo;est ce truc qui colle au haut (souvent) de la page lorsque l&rsquo;on <em>scroll<\/em> vers le bas.<\/p>\n<p>Je veux bien diviser le fond (HTML), la forme (CSS) et les interactions (JS), et dans cet ordre l\u00e0 pr\u00e9cis\u00e9ment. Je vais donc m&rsquo;interdire toute forme d&rsquo;animation ou de positionnement en JS, je vais utiliser CSS pour cela.<\/p>\n<p>Le comportement attendu est celui-ci : J&rsquo;ai un en-t\u00eate avec un logo et un menu qui sont \u00ab\u00a0classiquement\u00a0\u00bb positionn\u00e9s. Au del\u00e0 d&rsquo;un certain seuil de <em>scroll<\/em> (descente) dans la page, je veux r\u00e9-afficher le menu \u00e0 l&rsquo;utilisateur pour lui permettre un acc\u00e8s plus rapide. Dans un premier temps, la valeur de <em>scroll<\/em> sera arbitraire (d\u00e9finie manuellement par nos soins), puis dans un second temps nous ferons en sorte que le menu n&rsquo;apparaisse que lorsque le <em>scroll<\/em> aura pass\u00e9 un certain \u00e9l\u00e9ment dans la page. Enfin, nous verrons \u00e9galement une variante o\u00f9 le menu ne r\u00e9apparait que si l&rsquo;utilisateur remonte dans la page.<\/p>\n<p>Nous aurons \u00e0 la fin quelque chose dans ce go\u00fbt :<\/p>\n<p style=\"text-align: center;\"><a class=\"demo\" href=\"https:\/\/www.creativejuiz.fr\/blog\/doc\/sticky-menu.html\">D\u00e9monstration<\/a><\/p>\n<h2>La structure de notre menu <em>sticky<\/em><\/h2>\n<p>Je vous invite \u00e0 utiliser cette structure de menu, et \u00e9ventuellement \u00e0 copier\/coller le <em>Lorem Ipsum<\/em> de ma page de d\u00e9monstration pour avoir du contenu et pouvoir <em>scroll<\/em>er dans votre page.<\/p>\n<pre class=\"code\"><code class=\"language-markup\">&lt;header id=\"header\" role=\"banner\" class=\"main-header\"&gt;\r\n\t&lt;div class=\"header-inner\"&gt;\r\n \r\n\t\t&lt;div class=\"header-logo\"&gt;\r\n\t\t\t&lt;img src=\"logo.png\" alt=\"Creative Juiz\" width=\"150\" height=\"45\"&gt;\r\n\t\t&lt;\/div&gt;\r\n \r\n\t\t&lt;nav class=\"header-nav\"&gt;\r\n\t\t\t&lt;ul&gt;\r\n\t\t\t\t&lt;li&gt;&lt;a href=\"#\"&gt;Home&lt;\/a&gt;&lt;\/li&gt;\r\n\t\t\t\t&lt;li&gt;&lt;a href=\"#\"&gt;Our projects&lt;\/a&gt;&lt;\/li&gt;\r\n\t\t\t\t&lt;li&gt;&lt;a href=\"#\"&gt;About us&lt;\/a&gt;&lt;\/li&gt;\r\n\t\t\t\t&lt;li&gt;&lt;a href=\"#\"&gt;Contact&lt;\/a&gt;&lt;\/li&gt;\r\n\t\t\t&lt;\/ul&gt;\r\n\t\t&lt;\/nav&gt;\r\n \r\n\t&lt;\/div&gt;\r\n&lt;\/header&gt;<\/code><\/pre>\n<p>J&rsquo;utilise l&rsquo;\u00e9l\u00e9ment <code>&lt;header&gt;<\/code> avec le role <code>banner<\/code> pour d\u00e9finir l&rsquo;en-t\u00eate de ma page. Le reste est assez classique. Il est important de noter que l&rsquo;identifiant (<code>header<\/code>) va me servir ici en JS afin d&rsquo;acc\u00e9der rapidement \u00e0 l&rsquo;\u00e9l\u00e9ment.<\/p>\n<h2>Les styles de notre <em>sticky<\/em> menu<\/h2>\n<p>Ces styles vont \u00eatre assez sommaires et ne traitent pas le cas du <em>responsive<\/em>. Dans l&rsquo;id\u00e9al, l&rsquo;aspect sticky ne devrait \u00eatre \u00ab\u00a0activ\u00e9\u00a0\u00bb que sur des \u00e9crans assez larges et hauts pour \u00e9viter de g\u00eaner la visibilit\u00e9 du contenu.<\/p>\n<p><img decoding=\"async\" class=\"aligncenter size-full wp-image-5520\" src=\"https:\/\/www.creativejuiz.fr\/blog\/wp-content\/uploads\/2015\/11\/sticky-menu-demo.png\" alt=\"Sticky Header\" width=\"2728\" height=\"1538\" srcset=\"https:\/\/www.creativejuiz.fr\/blog\/wp-content\/uploads\/2015\/11\/sticky-menu-demo.png 2728w, https:\/\/www.creativejuiz.fr\/blog\/wp-content\/uploads\/2015\/11\/sticky-menu-demo-300x169.png 300w, https:\/\/www.creativejuiz.fr\/blog\/wp-content\/uploads\/2015\/11\/sticky-menu-demo-600x338.png 600w\" sizes=\"(max-width: 2728px) 100vw, 2728px\" \/><\/p>\n<p>Commen\u00e7ons par faire un mini reset de certains styles :<\/p>\n<pre class=\"code\"><code class=\"language-css\">\/* micro reset *\/\r\n* {\r\n\tbox-sizing: border-box;\r\n}\r\nhtml, body, ul {\r\n\tpadding: 0; margin: 0;\r\n}<\/code><\/pre>\n<p>Puis \u00e0 d\u00e9finir nos styles de base. Je vais utiliser les couleurs de mon blog, faites comme vous le souhaitez, bien entendu.<\/p>\n<pre class=\"code\"><code class=\"language-css\">\/* Quelques styles de base *\/\r\nbody {\r\n\tfont-family: Helvetica Neue, Helvetica, Arial, sans-serif;\r\n\tline-height: 1.75;\r\n\tcolor: #F1EEDD;\r\n\tbackground: #B13C2E;\r\n}\r\n.main-header {\r\n\tcolor: #B13C2E;\r\n\tbackground: #FFF;\r\n}<\/code><\/pre>\n<p>Vous pouvez actualiser votre page dans votre navigateur si vous suivez le tutoriel au fur et \u00e0 mesure, comme cela vous verrez pr\u00e9cis\u00e9ment les modifications que nous faisons.<br \/>\nNous allons maintenant placer nos \u00e9l\u00e9ments sur la m\u00eame ligne. Au lieu d&rsquo;utiliser le positionnement en flottant, j&rsquo;ai d\u00e9cid\u00e9 d&rsquo;utiliser <em>table layout<\/em> pour cette fois. Il serait \u00e9galement possible d&rsquo;utiliser flexbox, mais il faut bien faire un choix \ud83d\ude42<\/p>\n<pre class=\"code\"><code class=\"language-css\">\/* Les \u00e9l\u00e9ments sont plac\u00e9s l'un \u00e0 c\u00f4t\u00e9 de l'autre *\/\r\n.header-inner {\r\n\tdisplay: table;\r\n\twidth: 100%;\r\n\tmax-width: 1100px;\r\n\tmargin: 0 auto; \/* on centre l'\u00e9l\u00e9ment *\/\r\n\tpadding: 20px 25px; \/* on ventile un peu *\/\r\n}\r\n.header-inner &gt; * {\r\n\tdisplay: table-cell;\r\n\tvertical-align: middle;\r\n}<\/code><\/pre>\n<p>En utilisant ce positionnement en tableau sans utiliser <code>table-layout: fixed;<\/code> sur le parent, les cellules vont se dimensionner suivant le contenu de chacune. Profitons-en !<\/p>\n<p class=\"message remember\">La propri\u00e9t\u00e9 <code>table-layout: fixed;<\/code> permet de d\u00e9finir pr\u00e9cis\u00e9ment les dimensions de chaque cellule. Si vous l&rsquo;utilisez sans pr\u00e9cision des valeurs de <code>width<\/code> sur les enfants, ces derniers se r\u00e9partiront la largeur disponible, ici 50% chacun. Un tableau sans <code>table-layout: fixed;<\/code> met th\u00e9oriquement plus de temps \u00e0 \u00eatre affich\u00e9 par le navigateur. (on parle de micro\/milli secondes pour effectuer la r\u00e9partition des dimensions)<\/p>\n<p>Il va maintenant falloir passer notre liste de liens sur une seule ligne et espacer un peu les items les uns des autres. Il faut \u00e9galement que nous alignions le menu \u00e0 droite. Je vais vous montrer une astuce pour \u00e9viter d&rsquo;utiliser la propri\u00e9t\u00e9 <code>float<\/code>.<\/p>\n<pre class=\"code\"><code class=\"language-css\">\/* Alignement du menu *\/\r\n.header-nav {\r\n\ttext-align: right;\r\n}\r\n\/*\r\n   Faire passer le menu en inline (inline-block, inline-table ou inline-flex) pour le rendre sensible \u00e0 l'alignement \u00e0 droite. Ses items aussi sont en inline.\r\n*\/\r\n.header-nav ul,\r\n.header-nav li {\r\n\tdisplay: inline;\r\n\tlist-style: none;\r\n}\r\n.header-nav a {\r\n\tposition: relative;\r\n\tdisplay: inline-block;\r\n\tpadding: 8px 20px;\r\n\tvertical-align: middle;\r\n\tfont-weight: 300; \/* entre regular et light *\/\r\n\tletter-spacing: 0.025em;\r\n\tcolor: inherit;\r\n\ttext-decoration: none;\r\n}<\/code><\/pre>\n<p>Sur les liens, j&rsquo;ai d\u00e9cid\u00e9 de faire une petite animation au survol (et focus) : un trait de la taille du mot va venir s&rsquo;ajouter en remontant vers le mot en fondu. Pour cela je vais utiliser un pseudo-\u00e9l\u00e9ment <code>:after<\/code> pour g\u00e9n\u00e9rer un trait.<\/p>\n<pre class=\"code\"><code class=\"language-css\">\/* Animation du lien *\/\r\n.header-nav a:after {\r\n\tcontent: \"\";\r\n\tposition: absolute;\r\n\tbottom: 0; right: 20px; left: 20px;\r\n\theight: 2px;\r\n\tbackground-color: #B13C2E;\r\n\r\n\t\/* Pr\u00e9paration de notre animation *\/\r\n\topacity: 0;\r\n\ttransform: translateY(5px);\r\n\ttransition: all .4s;\r\n}\r\n\/* Le trait va remonter et apparaitre *\/\r\n.header-nav a:hover:after,\r\n.header-nav a:focus:after {\r\n\topacity: .6;\r\n\ttransform: translateY(0);\r\n}\r\n\/* Je vire outline car juste au-dessus je d\u00e9finis un style :focus *\/\r\n.header-nav a:focus {\r\n\toutline: none;\r\n}<\/code><\/pre>\n<p>Voil\u00e0, nous avons globalement un header minimaliste au go\u00fbt du jour et l&rsquo;aspect <em>sticky<\/em> de notre menu va pouvoir \u00eatre travaill\u00e9 gr\u00e2ce \u00e0 du JS. Mais avant cela, petite parenth\u00e8ses.<\/p>\n<h2>Menu <em>sticky<\/em> : la propri\u00e9t\u00e9 CSS <code>position: sticky;<\/code><\/h2>\n<p>La propri\u00e9t\u00e9 CSS <code>position<\/code> et sa valeur <code>sticky<\/code> ont \u00e9t\u00e9 introduites en CSS Level 3 mais a eu beaucoup de mal \u00e0 \u00eatre impl\u00e9ment\u00e9e dans nos navigateurs, et encore aujourd&rsquo;hui (\u00e0 l&rsquo;heure o\u00f9 j&rsquo;\u00e9cris ces lignes) seuls Firefox 32+ (et 41+ pour Android), Safari 6.1+ (pr\u00e9fixe -webkit-, et Safari iOS 6.1+) le supportent.<\/p>\n<p>Chrome a fait une tentative entre les versions 23 et 36 sous la forme d&rsquo;option d&rsquo;exp\u00e9rimentation \u00e0 activer (flag), mais a retir\u00e9 ce flag en version 37, depuis plus de nouvelle.<\/p>\n<p class=\"message\"><strong>Mise \u00e0 jour janvier 2017<\/strong> : Depuis sa version 56 Google Chrome supporte la position <code>sticky<\/code>.<\/p>\n<p>Cette valeur de propri\u00e9t\u00e9 est un m\u00e9lange de la valeur <code>fixed<\/code> et de la valeur <code>relative<\/code>.<br \/>\nEn effet, lorsque vous utilisez la valeur fixed, vous sortez l&rsquo;\u00e9l\u00e9ment du flux, c&rsquo;est \u00e0 dire que l&rsquo;\u00e9l\u00e9ment que vous positionnez va voir sa \u00ab\u00a0bo\u00eete\u00a0\u00bb sortie du calcul de positionnement g\u00e9n\u00e9ral des \u00e9l\u00e9ments les uns par rapport aux autres. Autrement dit, vous passez cet \u00e9l\u00e9ment sur un nouveau plan, il n&rsquo;est alors plus compt\u00e9 sur son plan d&rsquo;origine o\u00f9 les autres \u00e9l\u00e9ments vous faire leur petite vie sans lui.<\/p>\n<p>Si vous voulez faire un essai dans votre navigateur voici une petite explication (5 min) en vid\u00e9o.<\/p>\n<p><iframe src=\"https:\/\/www.youtube.com\/embed\/kzACYwrSWhQ?rel=0\" width=\"640\" height=\"480\" frameborder=\"0\" allowfullscreen=\"allowfullscreen\"><\/iframe><\/p>\n<p>Voici le code utilis\u00e9 :<\/p>\n<pre class=\"code\"><code class=\"language-css\">\/* Si seulement \u00e7a suffisait *\/\r\n.main-header {\r\n\tposition: sticky;\r\n\ttop: 0;\r\n}<\/code><\/pre>\n<p>Mais nous n&rsquo;allons pas l&rsquo;utiliser pour la suite de ce tutoriel.<\/p>\n<h2>Un peu de JavaScript !<\/h2>\n<p>Le JavaScript pr\u00e9sent\u00e9 ici est dit \u00ab\u00a0Vanilla\u00a0\u00bb, c&rsquo;est \u00e0 dire que le code est fonctionnel nativement dans vos navigateurs et n&rsquo;a pas besoin de jQuery pour fonctionner. Mais il peut quand m\u00eame fonctionner si vous utilisez une biblioth\u00e8que JS.<\/p>\n<p>Ce code est normalement fonctionnel \u00e0 partir de IE8 (non test\u00e9), mais j&rsquo;ose esp\u00e9rer que plus aucun de mes lecteurs ne supportent ce navigateur. (je dis \u00e7a pour votre bien et celui de vos clients)<\/p>\n<p>Pour commencer, dans votre document JS, inscrivez ce polyfill de <code>requestAnimationFrame()<\/code>.<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">\/*\r\n\tRequestAnimationFrame Polyfill\r\n\r\n\thttp:\/\/paulirish.com\/2011\/requestanimationframe-for-smart-animating\/\r\n\thttp:\/\/my.opera.com\/emoller\/blog\/2011\/12\/20\/requestanimationframe-for-smart-er-animating\r\n\tby Erik M\u00f6ller, fixes from Paul Irish and Tino Zijdel\r\n\r\n\tMIT license\r\n *\/ \r\n\r\n(function() {\r\n\tvar lastTime = 0;\r\n\tvar vendors = ['ms', 'moz', 'webkit', 'o'];\r\n\tfor(var x = 0; x &lt; vendors.length &amp;&amp; !window.requestAnimationFrame; ++x) {\r\n\t\twindow.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];\r\n\t\twindow.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];\r\n\t}\r\n\r\n\tif ( ! window.requestAnimationFrame ) {\r\n\t\twindow.requestAnimationFrame = function(callback, element) {\r\n\t\t\tvar currTime = new Date().getTime();\r\n\t\t\tvar timeToCall = Math.max(0, 16 - (currTime - lastTime));\r\n\t\t\tvar id = window.setTimeout(function() { callback(currTime + timeToCall); }, \r\n\t\t\ttimeToCall);\r\n\t\t\tlastTime = currTime + timeToCall;\r\n\t\t\treturn id;\r\n\t\t};\r\n\t}\r\n\r\n\tif ( ! window.cancelAnimationFrame ) {\r\n\t\twindow.cancelAnimationFrame = function(id) {\r\n\t\t\tclearTimeout(id);\r\n\t\t};\r\n\t}\r\n}());<\/code><\/pre>\n<p>Je ne vais pas d\u00e9tailler ce code, mais pour faire court si votre navigateur ne reconna\u00eet pas les fonctions natives <code>requestAnimationFrame()<\/code> et <code>cancelAnimationFrame()<\/code>, nous les cr\u00e9ons sur la base d&rsquo;un <code>setTimeout()<\/code>. Nous avions d\u00e9j\u00e0 vu une m\u00e9thode similaire sur l&rsquo;article : <a href=\"https:\/\/www.creativejuiz.fr\/blog\/tutoriels\/un-onresize-ou-onscroll-plus-performant-en-js\">Un <code>onresize<\/code> ou <code>onscroll<\/code> plus performant en JS<\/a>.<\/p>\n<p>Commen\u00e7ons par \u00e9crire notre code en le prot\u00e9geant. Ci-dessous, <code>w<\/code> et <code>d<\/code> correspondent \u00e0 <code>window<\/code> et <code>document<\/code> :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">(function(w,d,undefined){\r\n\u00a0\r\n\t\/\/ ici le code JS qui va suivre\r\n\u00a0\r\n}(window, document));<\/code><\/pre>\n<p>Le code que l&rsquo;on va ainsi ex\u00e9cuter sera prot\u00e9g\u00e9 des autres scripts pour \u00e9viter les conflits.<br \/>\nPlacez donc le code qui suit \u00e0 la place du commentaire du code pr\u00e9c\u00e9dent.<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">var el_html = d.documentElement,\r\n\tel_body = d.getElementsByTagName('body')[0],\r\n\theader = d.getElementById('header'),\r\n\tmenuIsStuck = function() {\r\n\t\t\/\/ Nous allons compl\u00e9ter notre code ici\r\n\t},\r\n\tonScrolling = function() {\r\n\t\t\/\/ on ex\u00e9cute notre fonction menuIsStuck()\r\n\t\t\/\/ dans la fonction onScrolling()\r\n\t\tmenuIsStuck();\r\n\t\t\/\/ on pourrait faire plein d'autres choses ici \r\n\t};\r\n\u00a0\r\n\/\/ quand on scroll\r\nw.addEventListener('scroll', function(){\r\n\t\/\/ on ex\u00e9cute la fonction onScrolling()\r\n\tw.requestAnimationFrame( onScrolling );\r\n});<\/code><\/pre>\n<p>Voyons ce code ensemble.<br \/>\nLes 11 premi\u00e8res lignes (le premier gros bloc) servent \u00e0 d\u00e9clarer quatre variables, dont deux sont en fait des fonctions.<br \/>\n<code>el_html<\/code> correspond au n\u0153ud <code>&lt;html&gt;<\/code> et <code>header<\/code> est le n\u0153ud de notre en-t\u00eate que nous allons positionner en sticky par la suite.<\/p>\n<p>La fonction <code>menuIsStuck<\/code> va nous permettre de faire les calculs de positionnement dans la page pour coller\/d\u00e9coller notre en-t\u00eate au bon moment. C&rsquo;est dans cette fonction que nous placerons le prochain bout de code JS.<br \/>\nLa fonction <code>onScrolling<\/code> va se d\u00e9clencher au moment du scroll dans la page, et ex\u00e9cutera \u00e0 son tour la fonction <code>menuIsStuck<\/code>. J&rsquo;ai l&rsquo;habitude d&rsquo;encapsuler certaines fonctions dans une plus globale, notamment lorsque j&rsquo;ai besoin d&rsquo;ex\u00e9cuter plusieurs fonctions lors du m\u00eame \u00e9v\u00e8nement, et\/ou lorsque certaines variables sont partag\u00e9es entre les fonctions.<\/p>\n<p>Le bloc qui suit sert \u00e0 d\u00e9clarer l&rsquo;\u00e9couteur d&rsquo;\u00e9v\u00e8nement, ici pour l&rsquo;\u00e9v\u00e8nement <code>scroll<\/code>. D\u00e8s qu&rsquo;un scroll est effectu\u00e9, la fonction <code>requestAnimationFrame()<\/code> va ex\u00e9cuter la fonction <code>onScrolling<\/code>. Cette premi\u00e8re fonction permet d&rsquo;ex\u00e9cuter aussi souvent que possible une action suivant les capacit\u00e9s du navigateur.<\/p>\n<p>Compl\u00e9tons notre fonction <code>menuIsStuck<\/code> en rempla\u00e7ant le commentaire par ce code :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">var wScrollTop\t= w.pageYOffset || el_body.scrollTop,\r\n\tregexp\t\t= \/(nav\\-is\\-stuck)\/i,\r\n\tclassFound\t= el_html.className.match( regexp ),\r\n\tnavHeight\t= header.offsetHeight,\r\n\tbodyRect\t= el_body.getBoundingClientRect(),\r\n\tscrollValue\t= 600;\r\n\u00a0\r\n\/\/ si le scroll est d'au moins 600 et\r\n\/\/ la class nav-is-stuck n'existe pas sur HTML\r\nif ( wScrollTop &gt; scrollValue &amp;&amp; !classFound ) {\r\n\tel_html.className = el_html.className + ' nav-is-stuck';\r\n\tel_body.style.paddingTop = navHeight + 'px';\r\n}\r\n\u00a0\r\n\/\/ si le scroll est inf\u00e9rieur \u00e0 2 et\r\n\/\/ la class nav-is-stuck existe\r\nif ( wScrollTop &lt; 2 &amp;&amp; classFound ) {\r\n\tel_html.className = el_html.className.replace( regexp, '' );\r\n\tel_body.style.paddingTop = '0';\r\n}<\/code><\/pre>\n<p>Nous avons ici trois blocs principaux de code : une d\u00e9claration de variables diverses et vari\u00e9es, et deux contr\u00f4les.<\/p>\n<p>La d\u00e9claration r\u00e9unit tous nos besoins en calculs et variables.<\/p>\n<ul>\n<li><code>wScrollTop<\/code> est la position de notre scroll dans la page.<\/li>\n<li><code>regexp<\/code> permet d&rsquo;enregistrer l&rsquo;expression r\u00e9guli\u00e8re qui va nous permettre de d\u00e9tecter la classe CSS <code>nav-is-stuck<\/code> que nous allons utiliser.<\/li>\n<li><code>classFound<\/code> enregistre si oui ou non l&rsquo;\u00e9l\u00e9ment HTML est porteur de notre classe CSS.<\/li>\n<li><code>navHeight<\/code> enregistre la hauteur de notre header (oui \u00e7a peut varier).<\/li>\n<li><code>bodyRect<\/code> nous retourne plusieurs valeurs li\u00e9e \u00e0 notre \u00e9l\u00e9ment body (comme la largeur, la hauteur, la position dans la page, etc.).<\/li>\n<li><code>scrollValue<\/code> est la distance que nous avons choisi pour d\u00e9clencher l&rsquo;effet sticky, c&rsquo;est \u00e0 dire ici 600px.<\/li>\n<\/ul>\n<p>Une fois ces variables enregistr\u00e9es ou mises \u00e0 jour (elles le seront pour certaines \u00e0 chaque mouvement de scroll), nous effectuons deux contr\u00f4les qui ne peuvent th\u00e9oriquement pas \u00eatre \u00ab\u00a0vrais\u00a0\u00bb en m\u00eame temps.<\/p>\n<p>Le premier contr\u00f4le permet de v\u00e9rifier lorsque l&rsquo;on arrive au seuil de <em>scroll<\/em> que l&rsquo;on a d\u00e9clar\u00e9. D\u00e8s que c&rsquo;est le cas, on ajoute la class <code>nav-is-stuck<\/code> sur l&rsquo;\u00e9l\u00e9ment HTML et on ajoute un <code>padding<\/code> \u00e0 <code>body<\/code> \u00e9quivalent \u00e0 la hauteur du header. En effet si nous ne faisons pas cet ajout de <code>padding<\/code>, nous allons nous retrouver avec un effet du bord (un saut bizarre) d\u00fb \u00e0 la sortie du flux du header. (voir la vid\u00e9o ci-dessus)<\/p>\n<p>Le second contr\u00f4le permet de v\u00e9rifier lorsque l&rsquo;on arrive en haut de la page. D\u00e8s que c&rsquo;est le cas, on retire la classe et le <code>padding<\/code> pour replacer le header dans le flux, \u00e0 sa position d&rsquo;origine.<\/p>\n<p>Dans les deux cas, avant d&rsquo;ajouter la classe et le <code>padding<\/code>, on v\u00e9rifie simplement que la classe n&rsquo;est pas d\u00e9j\u00e0 pr\u00e9sente, \u00e7a \u00e9vite de faire l&rsquo;action inutilement. Idem dans l&rsquo;autre sens, avant de la retirer, on v\u00e9rifie si elle n&rsquo;a pas d\u00e9j\u00e0 \u00e9t\u00e9 retir\u00e9e.<\/p>\n<p>Voici le code JS complet pour cette partie, sans le polyfill.<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">(function(w,d,undefined){\r\n\u00a0\r\n\tvar el_html = d.documentElement,\r\n\t\tel_body = d.getElementsByTagName('body')[0],\r\n\t\theader = d.getElementById('header'),\r\n\t\tmenuIsStuck = function() {\r\n\r\n\r\n\t\t\tvar wScrollTop\t= w.pageYOffset || el_body.scrollTop,\r\n\t\t\t\tregexp\t\t= \/(nav\\-is\\-stuck)\/i,\r\n\t\t\t\tclassFound\t= el_html.className.match( regexp ),\r\n\t\t\t\tnavHeight\t= header.offsetHeight,\r\n\t\t\t\tbodyRect\t= el_body.getBoundingClientRect(),\r\n\t\t\t\tscrollValue\t= 600;\r\n\u00a0\r\n\t\t\t\/\/ si le scroll est d'au moins 600 et\r\n\t\t\t\/\/ la class nav-is-stuck n'existe pas sur HTML\r\n\t\t\tif ( wScrollTop &gt; scrollValue &amp;&amp; !classFound ) {\r\n\t\t\t\tel_html.className = el_html.className + ' nav-is-stuck';\r\n\t\t\t\tel_body.style.paddingTop = navHeight + 'px';\r\n\t\t\t}\r\n\u00a0\r\n\t\t\t\/\/ si le scroll est inf\u00e9rieur \u00e0 2 et\r\n\t\t\t\/\/ la class nav-is-stuck existe\r\n\t\t\tif ( wScrollTop &lt; 2 &amp;&amp; classFound ) {\r\n\t\t\t\tel_html.className = el_html.className.replace( regexp, '' );\r\n\t\t\t\tel_body.style.paddingTop = '0';\r\n\t\t\t}\r\n\r\n\t\t},\r\n\t\tonScrolling = function() {\r\n\t\t\t\/\/ on ex\u00e9cute notre fonction menuIsStuck()\r\n\t\t\t\/\/ dans la fonction onScrolling()\r\n\t\t\tmenuIsStuck();\r\n\t\t\t\/\/ on pourrait faire plein d'autres choses ici \r\n\t\t};\r\n\u00a0\r\n\t\/\/ quand on scroll\r\n\tw.addEventListener('scroll', function(){\r\n\t\t\/\/ on ex\u00e9cute la fonction onScrolling()\r\n\t\tw.requestAnimationFrame( onScrolling );\r\n\t});\r\n\u00a0\r\n}(window, document));<\/code><\/pre>\n<p>C&rsquo;est long \u00e0 expliquer pour moi et \u00e0 int\u00e9grer peut-\u00eatre pour vous, mais finalement cela ne fait pas beaucoup de code \u00e0 \u00e9crire \ud83d\ude42<\/p>\n<p>Ok\u2026 mais on a toujours pas de sticky avec \u00e7a, juste un changement de classe CSS et un <code>padding<\/code> en plus. Passons au compl\u00e9ment de CSS.<\/p>\n<h2>Notre menu <em>sticky<\/em> anim\u00e9<\/h2>\n<p>Nous allons utiliser uniquement du CSS pour faire notre animation, et si jamais l&rsquo;animation CSS n&rsquo;est pas comprise par le navigateur (ce qui devient rare), et bien il n&rsquo;en aura pas, tout simplement.<\/p>\n<pre class=\"code\"><code class=\"language-css\">.nav-is-stuck .main-header {\r\n\tposition: fixed;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\tright: 0;\r\n\tbox-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);\r\n\tanimation: stickAnim .3s;\r\n}\r\n\u00a0\r\n@keyframes stickAnim {\r\n\t0% {\r\n\t\ttransform: translateY(-86px);\r\n\t}\r\n\t100% {\r\n\t\ttransform: translateY(0);\r\n\t}\r\n}<\/code><\/pre>\n<p>D\u00e8s que l&rsquo;\u00e9l\u00e9ment HTML poss\u00e8de la classe <code>nav-is-stuck<\/code>, je peux cibler le header gr\u00e2ce au s\u00e9lecteur de descendance et le fixer. Tout en le fixant, je lui attribue l&rsquo;animation <code>stickAnim<\/code> d\u00e9clar\u00e9e juste apr\u00e8s.<br \/>\nCette animation fait faire une translation CSS en Y de -86px (environ la hauteur du menu) vers 0, son emplacement d&rsquo;origine. Cela nous donne l&rsquo;effet exact de <a href=\"https:\/\/www.creativejuiz.fr\/blog\/doc\/sticky-menu.html\">la d\u00e9mo<\/a>.<\/p>\n<p>Et voil\u00e0, vous \u00eates arriv\u00e9s au bout de ce tutoriel !<\/p>\n<p>Pour les plus courageux, nous allons voir un petit compl\u00e9ment de notre code JS pour d\u00e9finir le d\u00e9clenchement du sticky seulement lorsque le <em>scroll<\/em> atteint un autre \u00e9l\u00e9ment de la page.<\/p>\n<h2>Bonus\u00a0: D\u00e9clencher le <em>sticky<\/em> quand il atteint un \u00e9l\u00e9ment<\/h2>\n<p>Il va s&rsquo;agir pour nous de rendre dynamique la valeur de la variable <code>scrollValue<\/code> en la rempla\u00e7ant par la valeur en pixel de l&rsquo;\u00e9l\u00e9ment HTML d\u00e9clencheur. Dans mon code de d\u00e9mo, j&rsquo;ai d\u00e9fini plusieurs autres sections et \u00e9l\u00e9ments HTML. Je vous invite \u00e0 faire de m\u00eame. Attribuez un identifiant (attribut <code>id<\/code>) \u00e0 l&rsquo;un de ces \u00e9l\u00e9ments, nous en aurons besoin.<\/p>\n<p>Dans la fonction <code>onScrolling()<\/code>, rep\u00e9rez l&rsquo;appel \u00e0 <code>menuIsStuck()<\/code> et ajoutez en param\u00e8tre <code>d.getElementById('VOTRE_ID')<\/code> o\u00f9 <code>VOTRE_ID<\/code> est l&rsquo;identifiant de l&rsquo;\u00e9l\u00e9ment HTML d\u00e9clencheur.<br \/>\nLa fonction ressemble alors \u00e0 cela :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">onScrolling = function() {\r\n\tmenuIsStuck( d.getElementById('main') );\r\n};<\/code><\/pre>\n<p>Il faut que nous ajoutions ce param\u00e8tre \u00e0 notre fonction menuIsStuck (plus haut dans le code). La ligne qui d\u00e9clare la fonction ressemble alors \u00e0 cela :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">menuIsStuck = function(triggerElement) {<\/code><\/pre>\n<p>Cette variable <code>triggerElement<\/code> est r\u00e9cup\u00e9rable en l&rsquo;\u00e9tat dans notre code \u00e0 l&rsquo;int\u00e9rieur de la fonction. Il ne nous reste plus qu&rsquo;\u00e0 \u00e9diter la valeur de la variable <code>srollValue<\/code> comme suit :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">scrollValue\t= triggerElement ? triggerElement.getBoundingClientRect().top - bodyRect.top - navHeight  : 600,<\/code><\/pre>\n<p>\u00c7a vous parle ? Je pourrais comprendre que non. Mais d\u00e9taillons. Il s&rsquo;agit d&rsquo;un <a href=\"https:\/\/developer.mozilla.org\/fr\/docs\/Web\/JavaScript\/Reference\/Op%C3%A9rateurs\/L_op%C3%A9rateur_conditionnel\">op\u00e9rateur ternaire<\/a>, qui est repr\u00e9sent\u00e9 sous cette forme :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">variable = condition ? valeur_si_vrai : valeur_si_faux<\/code><\/pre>\n<p>Dans notre cas on v\u00e9rifie que <code>triggerElement<\/code> vaut bien quelque chose, si c&rsquo;est le cas on fait un calcul savant pour r\u00e9cup\u00e9rer la valeur \u00ab\u00a0top\u00a0\u00bb de l&rsquo;\u00e9l\u00e9ment (le haut quoi), autrement on utilise notre valeur arbitraire de 600 pixels.<\/p>\n<p>C&rsquo;est tout !<\/p>\n<h2 id=\"sticky-back\">Bonus\u00a0: Faire appara\u00eetre le menu quand on remonte dans la page<\/h2>\n<p>Le deuxi\u00e8me petit bonus que je vous avais propos\u00e9 \u00e9tait de ne faire appara\u00eetre le menu en <code>sticky<\/code> que lorsque l&rsquo;utilisateur d\u00e9cide de remonter dans la page, donc quand il <em>scroll<\/em> vers le haut.<\/p>\n<p class=\"center\"><a class=\"demo\" href=\"https:\/\/www.creativejuiz.fr\/blog\/doc\/sticky-back-menu.html\">D\u00e9monstration<\/a><\/p>\n<p>Nous allons devoir \u00e0 nouveau \u00e9diter notre code JS, mais tout ne va pas \u00eatre identique au pr\u00e9c\u00e9dent. Commen\u00e7ons par \u00e9diter l\u00e9g\u00e8rement notre d\u00e9claration de variables en tout d\u00e9but de code.<\/p>\n<p>Vous avez normalement la m\u00eame chose au d\u00e9but que ce bloc de code, ajoutez simplement la ligne concernant le <code>lastScroll<\/code>.<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">var el_html = d.documentElement,\r\n\tel_body = d.getElementsByTagName('body')[0],\r\n\theader = d.getElementById('header'),\r\n\tlastScroll = w.pageYOffset || el_body.scrollTop,<\/code><\/pre>\n<p>Cela nous permet d&rsquo;initialiser la valeur de cette variable. Souvent cela correspondra \u00e0 0, soit le haut de la page.<br \/>\nMaintenant nous allons utiliser cette variable dans la fonction <code>onScrolling()<\/code> comme suit :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">onScrolling = function() {\r\n\t\/\/ on r\u00e9cup\u00e8re la valeur du scroll maintenant\r\n\tvar wScrollTop = w.pageYOffset || el_body.scrollTop;\r\n\t\t\r\n\t\/\/ on ajoute deux arguments, valeurs de scrolls\r\n\tmenuIsStuck( d.getElementById('main'), wScrollTop, lastScroll );\r\n\t\t\t\r\n\t\/\/ on enregistre notre derni\u00e8re valeur de scroll\r\n\tlastScroll = wScrollTop;\r\n\t\t\t\r\n};<\/code><\/pre>\n<p>Ici on ajoute une ligne avant et une ligne apr\u00e8s notre appel \u00e0 <code>menuIsStuck()<\/code> pour r\u00e9cup\u00e9rer la valeur courante du scroll, puis enregistrer cette valeur comme ancienne valeur de <em>scroll<\/em>, cela va nous permettre de savoir si on monte ou descend lorsque l&rsquo;on bouge dans la page.<\/p>\n<p>Notre fonction <code>menuIsStuck()<\/code> accueille maintenant 3 param\u00e8tres : l&rsquo;\u00e9l\u00e9ment cible, la valeur de <em>scroll<\/em> et la valeur de <em>scroll<\/em> pr\u00e9c\u00e9dente. Il faut donc \u00e9diter notre fonction pour utiliser ces param\u00e8tres.<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">menuIsStuck = function(triggerElement, wScrollTop, lastScroll) {<\/code><\/pre>\n<p>Sur la ligne de la d\u00e9claration, vous ajoutez donc ces deux param\u00e8tres.<br \/>\nJuste en-dessous, retirez la d\u00e9claration de <code>wScrollTop<\/code>, elle fait doublon maintenant, on l&rsquo;a d\u00e9j\u00e0 en param\u00e8tre. (n&rsquo;effacez pas le <code>var<\/code>)<\/p>\n<p>Conservez tout le reste, il nous faut juste modifier nos deux contr\u00f4les (les <code>if<\/code>). En gros il faut que l&rsquo;on ajoute les conditions \u00ab\u00a0si on remonte\u00a0\u00bb et \u00ab\u00a0si on descend\u00a0\u00bb. On va faire appara\u00eetre notre menu uniquement si on descend, et qu&rsquo;on a atteint notre seuil (m\u00eame seuil qu&rsquo;avant), notre premier contr\u00f4le va donc se transformer en :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">if ( wScrollTop &gt; scrollValFix &amp;&amp; !classFound &amp;&amp; wScrollTop &lt; lastScroll ) {<\/code><\/pre>\n<p>Ici <code>wScrollTop &lt; lastScroll<\/code> permet de dire \u00ab\u00a0on remonte\u00a0\u00bb.<\/p>\n<p>Le second contr\u00f4le va ressembler \u00e0 \u00e7a :<\/p>\n<pre class=\"code\"><code class=\"language-javascript\">if ( classFound &amp;&amp; wScrollTop &gt; lastScroll ) {<\/code><\/pre>\n<p>Et voil\u00e0, notre <em>sticky<\/em> menu n&rsquo;apparait d\u00e9sormais que lorsque l&rsquo;on remonte dans la page, et disparait si l&rsquo;on descend.<\/p>\n<p>J&rsquo;esp\u00e8re que ce long tutoriel vous aura plu. N&rsquo;h\u00e9sitez pas \u00e0 commenter pour nous partager vos essais et vos d\u00e9mos.<\/p>\n<p>Et si coder vous ennuie, il existe le bon plugin <a href=\"http:\/\/wicky.nillia.ms\/headroom.js\/\">Headroom.js<\/a>. (merci <a href=\"https:\/\/twitter.com\/IAmNotCyril\/status\/664764538160566272\">@IamNotCyril<\/a> pour la suggestion)<\/p>\n<div class=\"sources\">\n<h2>Sources et liens utiles<\/h2>\n<ul>\n<li><a href=\"http:\/\/caniuse.com\/#feat=css-sticky\">Can I Use: Sticky<\/a><\/li>\n<li><a href=\"https:\/\/drafts.csswg.org\/css-position\/Overview.html#sticky-pos\">CSS WG Sticky<\/a><\/li>\n<li><a href=\"http:\/\/wicky.nillia.ms\/headroom.js\/\">Headroom.js<\/a><\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>J&rsquo;ai boss\u00e9 r\u00e9cemment sur plusieurs sites web, et la requ\u00eate d&rsquo;un menu sticky \u00e9tait quasiment syst\u00e9matique. Parfois elle \u00e9tait justifi\u00e9e, parfois je me rapprochais du contre-exemple ergonomique st\u00e9r\u00e9otyp\u00e9. (Dois-je utiliser un sticky menu ?) Mais quand m\u00eame ! Je vous propose de voir ensemble comment on peut faire \u00e7a.<\/p>\n","protected":false},"author":4,"featured_media":5519,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_bluesky_dont_syndicate":"","_bluesky_syndication_accounts":"","_bluesky_syndication_text":"","footnotes":""},"categories":[17,610,9],"tags":[157,688,105],"coauthors":[597],"class_list":["post-5491","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-css-css3","category-javascript","category-tutoriels","tag-menu","tag-sticky","tag-video"],"acf":[],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/posts\/5491","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/comments?post=5491"}],"version-history":[{"count":0,"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/posts\/5491\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/media\/5519"}],"wp:attachment":[{"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/media?parent=5491"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/categories?post=5491"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/tags?post=5491"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.creativejuiz.fr\/blog\/wp-json\/wp\/v2\/coauthors?post=5491"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}