Com vaig utilitzar noves functionalitats de CSS per millorar la UX de la barra de navegació de Coding Kittens

20 de gener del 2024

Atenció! Funcionalitat experimental

Al moment d’escriure aquesta entrada la funcionalitat de animation-timeline:scroll encara és experimental. Assegura’t que afegeixes una alternativa compatible. Pots consultar la llista de compatibilitat a caniuse.com

He treballat en moltes iteracions de la barra de navegació d’aquesta web per aconseguir un estil el qual em satisfés. Volia un logo que destaques i amb una navegació sempre visible — especialment a mòbil — perquè la persona llegint aquesta entrada pogués interactuar amb ella en tot moment.

Per aquest motiu vaig decidir que la barra de navegació s’adherís a la part superior quan l’usuari es desplacés i vaig afegir el logo amb el nom de la web i l’eslògan.

Preciós, — des del meu punt de vista — per fi estava content amb el resultat.

git commit -m "implement absolute perfect non-AI Navbar"

Després del desplegament automàtic, em disposo a consultar al meu mòbil com ha quedat la web. Tot pareixia fantàstic fins que vaig començar a llegir la meva primera entrada al blog Com arreglar l’error “Stack-Size limit” d’una funció recursiva mitjançant iteració a JavaScript i em vaig adonar que al meu mòbil diminut amb la meva pantalla minúscula la barra de navegació ocupava gran part de l’espai de lectura.

Llavors tenia un mòbil de 4,7 polzades així que normalment no tenia molt d’espai per llegir.

Mira.

La web de Coding Kittens amb una barra de navegació ocupant un percentatge molt alt de la pantalla

La barra de navegació ocupa un percentatge molt alt de la pantalla a un dispositiu petit

Sent alguna cosa similar a la claustrofòbia quan tenc un text contingut a un espai tan diminut.

Tot i això, no volia que la barra de navegació destaques menys — la primera impressió quan arribés a una web és la més important — però no podía deixar-ho així.

Després vaig pensar… “D’acord, si em molesta quan em desplaç cap avall per llegir la entrada del blog llavors… què tal si faig que la barra de navegació sigui més petita quan l’usuari no és a dalt de tot de la pàgina?”.

I això és el que vaig fer.

D’acord, després d’aquesta petita introducció i donat tot el context anem a fer un viatge al món del CSS i el JavaScript, on l’impossible és possible i on hi ha mil maneres d’arribar allà mateix’. 🪄🔮

Si fas una recerca ràpida a google sobre com encollir el menú de navegació a mesura que l’usuari es desplaça cap avall el primer resultat que et sortirà segurament serà aquesta pàgina de W3schools: How TO - Shrink Navigation Menu on Scroll .

Les seccions d’HTML i CSS són la base d’una barra de navegació. La part interessant és aquest trosset de JavaScript:

// Quan l'usuari es desplaça 80px des de la part de dalt del document, canviam la mida del padding i de la font del logo.
window.onscroll = function () {
  scrollFunction();
};
 
function scrollFunction() {
  if (document.body.scrollTop > 80 || document.documentElement.scrollTop > 80) {
    document.getElementById('navbar').style.padding = '30px 10px';
    document.getElementById('logo').style.fontSize = '25px';
  } else {
    document.getElementById('navbar').style.padding = '80px 10px';
    document.getElementById('logo').style.fontSize = '35px';
  }
}

Aquest codi bàsicament assigna una funció a l’event global onscroll i comprova quina proporció del <body> o del <html> l’usuari s’ha desplaçat. Aplicarà uns estils o uns altres en funció de la distància.

Això és tot, gràcies per llegir aquesta entrada.

Ara de debò, aquest intent de solucionar el problema és el més simple, i probablement una bona opció en la majoria dels casos.

El problema és que… on està la gràcia en fer-ho així? Coding Kittens és un lloc on m’agrada experimentar i provar coses noves. Vull una solució òptima, extensible i basada exclusivament en CSS

Com et quedaries si et dic que hi ha una manera futurista d’enfocar la solució utilitzant exclusivament CSS? Agafaries la píndola avorrida o la píndola futurística ?

Si sents curiositat explicaré a la pròxima secció una funció experimental que ens ajudarà a implementar aquesta funcionalitat.

La majoria de funcionalitats que farem servir tenen un 96% de suport dels navegadors segons la web Can I use . Per altra banda, la funcionalitat important, animation-timeline:scroll, és menys suportada, però explicarem com afegir una alternativa per a navegadors que no la suporten.

Així doncs, començarem explicant que és animation-timeline:scroll.

Seré honest, al moment d’escriure això anava a donar una explicació detallada de com funciona, però m’he adonat que tot el que puc dir ja està dit com toca a distintes fonts on he trobat aquesta informació.

A Scroll Progress Timeline is an animation timeline that is linked to progress in the scroll position of a scroll container–also called scrollport or scroller–along a particular axis. It converts a position in a scroll range into a percentage of progress.

Perquè ens entenguem, ScrollTimeline permet executar una animació en funció de la posició de desplaçament actual. Exclusivament amb CSS.

Integrating scroll-driven animations with two existing APIs, means that they benefit from the advantages of these APIs. That includes the ability to have these animations run off the main thread. Yes, read that correctly: you can now have silky smooth animations, driven by scroll, running off the main thread, with just a few lines of extra code. What’s not to like?!

Això m’explota el cap 🤯

El que vol dir és que aquesta animació no interromp el fil principal de JavaScript, adeu animacions que petardegen!

Pots comprovar el cas d’estudi que he compartit abans si vols veure una bona comparativa del rendiment entre JavaScript tradicional en comparació a animation-timeline.

Ara, doncs, comencem a fer una mica de màgia.

Al següent exemple he creat una barra de navegació similar a la que he creat per la web de Coding Kittens. Començaré amb l’HTML bàsic.

index.html
... codi HTML aquí
<nav class="navbar">
  <div class="backdrop"></div>
  <div class="navbar-content">
    <a href="/" class="logo-link">
      <!--Logo SVG -->
      <div class="site-name">
        <p>Your site</p>
      </div>
    </a>
    <div class="navigation-links-container">
      <div class="navigation-links">
        <a>Home</a>
        <a>Blog</a>
      </div>
    </div>
  </div>
</nav>
... més codi HTML aquí

Després, necessitarem afegir alguns estils a aquest element per fer que sigui semblant a una barra de navegació.

styles.css
.navbar {
  display: flex;
  position: sticky;
  padding: 1rem 0.5rem;
  margin-bottom: 3rem;
  margin-top: 5rem;
  flex-direction: row;
  align-items: flex-start;
  top: 0;
 
  @media (min-width: 768px) {
    padding: 1rem 1.25rem;
  }
}
 
.backdrop {
  position: absolute;
  top: 0;
  min-width: 100%;
  height: 5rem;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  left: -8px;
  right: -8px;
 
  @media (min-width: 768px) {
    margin-right: -28px;
    margin-left: -28px;
  }
}
 
.navbar-content {
  display: flex;
  z-index: 10;
  margin-left: 0;
  flex-direction: row;
  justify-content: space-between;
  min-width: 100%;
}
 
.logo-link {
  display: flex;
  flex-direction: row;
  align-items: center;
  text-decoration: none;
}
 
.logo {
  margin-right: 1rem;
}
 
.site-name {
  display: flex;
  flex-direction: column;
}
 
.site-name > p {
  color: white;
  margin: 0;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;
 
  @media (min-width: 640px) {
    font-size: 1.5rem;
    line-height: 2rem;
  }
}
 
.navigation-links-container {
  display: flex;
  flex-direction: row-reverse;
  gap: 1.5rem;
 
  @media (min-width: 640px) {
    flex-direction: row;
  }
}
 
.navigation-links {
  display: none;
  flex-direction: row;
  align-items: center;
 
  @media (min-width: 640px) {
    display: flex;
  }
}
 
.navigation-links a {
  position: relative;
  padding: 0.25rem 0.5rem;
}

Un cop tenguem totes aquestes peces juntes tendrem el següent:

.navbar {
    display: flex;
    position: sticky;
    padding: 1rem 0.5rem;
    margin-bottom: 3rem;
    margin-top: 5rem;
    flex-direction: row;
    align-items: flex-start;
    top: 0;
  
    @media (min-width: 768px) {
      padding: 1rem 1.25rem;
    }
  }
  
  .backdrop {
    position: absolute;
    top: 0;
    min-width: 100%;
    height: 5rem;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    left: -8px;
    right: -8px;
  
    @media (min-width: 768px) {
      margin-right: -28px;
      margin-left: -28px;
    }
  }
  
  
.navbar-content {
  display: flex;
  z-index: 10;
  margin-left: 0;
  flex-direction: row;
  justify-content: space-between;
  min-width: 100%;
}

.logo {
  margin-right: 1rem;
}

.logo-link {
  display: flex;
  flex-direction: row;
  align-items: center;
  text-decoration: none;
}

.site-name {
  display: flex;
  flex-direction: column;
}

.site-name > p {
  color: white;
  margin: 0;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;

  @media (min-width: 640px) {
    font-size: 1.5rem;
    line-height: 2rem;
  }
}

.navigation-links-container {
  display: flex;
  flex-direction: row-reverse;
  gap: 1.5rem;

  @media (min-width: 640px) {
    flex-direction: row;
  }
}

.navigation-links {
  display: none;
  flex-direction: row;
  align-items: center;

  @media (min-width: 640px) {
    display: flex;
  }
}

.navigation-links a {
  position: relative;
  padding: 0.25rem 0.5rem;
}

.scrollable-container {
  min-height: 150dvh;
}

body {
  font-family: sans-serif;
  -webkit-font-smoothing: auto;
  -moz-font-smoothing: auto;
  -moz-osx-font-smoothing: grayscale;
  font-smoothing: auto;
  text-rendering: optimizeLegibility;
  font-smooth: always;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;

  background-color: color-mix(in srgb, slategrey 100%, black 100%);
  color: white;
}

Tenim la nostra primera versió de la barra de navegació, similar a la mencionada al principi d’aquesta entrada del blog . Encara no hem afegit l’animació i ocupa un gran percentatge de la pantalla. Ho podem fer millor.

Aplicarem uns quants trucs de màgia de CSS 🪄

styles.css
  @keyframes shrink {
    0% {
      --navbar-shrink: 0;
    }
    10%,
    100% {
      --navbar-shrink: 1;
    }
  }
 
.navbar {
    display: flex;
    position: sticky;
    padding: 1rem 0.5rem;
    margin-bottom: 3rem;
    margin-top: 5rem;
    flex-direction: row;
    align-items: flex-start;
    top: 0;
  
    @media (min-width: 768px) {
      padding: 1rem 1.25rem;
    }
 
    --navbar-shrink: 0; 
    animation: shrink; 
    animation-timing-function: ease; 
    animation-timeline: scroll(nearest block); 
}

Explicaré pas per pas que és el que esteim fent aquí:

  1. Primer definim els keyframes de la nostra animació. Els keyframes canviaran una variable de CSS que hem anomenat --navbar-shrink. Utilitzarem aquest valor com el nostre valor condicional per alternar entre la navegació escurçada o normal.

  2. Seguidament inicialitzam --navbar-shrink a 0

  3. Després assignam shrink a l’animació

  4. Com a timing function farem servir ease. Però pots utilitzar el valor que vulguis d’aquesta llista

  5. Finalment definim animation-timeline per cridar la funció de CSS scroll amb nearest com a scrollable i block com a axis.

Remember

animation-timeline ha de ser declarat després de la propietat abreviada animation, ja que aquesta reiniciarà les propietats no incloses i no abreviades al seu valor inicial.

El codi que acabam d’afegir ens permet utilitzar la variable de CSS que utilitzam de condicional per aplicar distints estats a la nostra barra de navegació en funció de la posició de desplaçament de l’usuari. Com et quedes?

Ara afegirem algunes animacions interessants:

styles.css
... estils de .navbar ... 
 
.navbar .navbar-content {
  transition: transform 0.3s ease;
  transform: translateY(calc(0px - var(--navbar-shrink) * 12px));
  /* calc(initial_position - (0 * 12 || 1 * 12))  */
}
.navbar .backdrop {
  transition: transform 0.3s ease;
  transform: translateY(calc(0px - var(--navbar-shrink) * 24px));
  /* calc(initial_position - (0 * 24 || 1 * 24))  */
}
 
.navbar .logo {
  transition: transform 0.3s ease;
  transform: scale(calc(1 - var(--navbar-shrink) * 0.2));
  /* calc(initial_scale - (0 * 0.2 || 1 * 0.2))  */
}
 
.navbar p {
  transition: opacity 0.3s ease;
  opacity: calc(1 - var(--navbar-shrink));
  /* calc(initial_opacity - (0 || 1))  */
}
... més estils ...

Com es veu utilitzam la variable --navbar-shrink per calcular dinàmicament el valor utilitzant la funció de CSS calc. Això és el que està fent:

  • Moure el navbar-content i el backdrop uns quants píxels fora de la pantalla.

  • Redueix la mida del logo

  • Redueix l’opacitat del text

Aquest codi es pot estendre per incloure totes les transicions que vulguem.

Fixa’t com he fet servir scale, opacity i translateY per aprofitar l’animació composta .

Com he mencionat al principi d’aquesta entrada, la propietat de CSS animation-timeline encara no està suportada a tots els navegadors. De fet, a gener del 2024 domés els navegadors Chromium i Firfeox (fent ús d’una opció de configuració experimental) suporten aquesta propietat.

No tenguis por, tenc la solució ideal per tu. Farem alguns canvis al nostre CSS i afegirem una mica de JavaScript.

Començarem afegint la regla “CSS @supports” per comprovar si el navegador suporta la funció scroll().

styles.css
.navbar {
  display: flex;
  position: sticky;
  padding: 1rem 0.5rem;
  margin-bottom: 3rem;
  margin-top: 5rem;
  flex-direction: row;
  align-items: flex-start;
  top: 0;
 
  @media (min-width: 768px) {
    padding: 1rem 1.25rem;
  } 
 
   --navbar-shrink: 0;

    animation: shrink;
    animation-timeline: scroll(nearest block);
    animation-timing-function: ease;
}
 
  @supports (animation-timeline: scroll()) {
    .navbar {
      --navbar-shrink: 0;

      animation: shrink;
      animation-timeline: scroll(nearest block);
      animation-timing-function: ease;
    }
  }
    

A continuació, si no hi ha suport, necessitam alternar la variable --navbar-shrink de totes maneres, ja que encara necessitam canviar els estils de forma dinàmica als elements de la barra de navegació.

Afegirem una mica de JavaScript davall el nostre element nav perquè sigui executat després que l’element sigui renderitzat a la pàgina. Idealment, també es pot afegir just abans del tancament de l’etiqueta </body>.

index.html
... html code here
<nav class="navbar">
  <div class="backdrop"></div>
  <div class="inner-container">
    <a href="/" class="logo-link">
      <!--Logo SVG -->       
      <div class="site-name">
        <p>Your site</p>
      </div>
    </a>
    <div class="navigation-links">
      <div class="navigation-links-inner">
        <a>Home</a>
        <a>Blog</a>
      </div>
    </div>
  </div>
</nav>
<script>
  (function () {
    const navbar = document.querySelector(".navbar");
 
    if (!CSS.supports("animation-timeline: scroll()")) {
      const threshold = 500; // Adjust this threshold as needed
 
      window.addEventListener("scroll", handleScroll);
 
      function handleScroll() {
        if (!navbar) {
          return;
        }
 
        const scrollY = window.scrollY || window.pageYOffset;
 
        if (scrollY > threshold) {
          navbar.style.setProperty("--navbar-shrink", "1");
        } else {
          navbar.style.setProperty("--navbar-shrink", "0");
        }
      }
    }
  })();
</script>
... més codi HTML

Aquest codi executa la implementació que us he mostrat al principi d’aquesta entrada .

Si vols, pots comprovar el resultat dels canvis que hem aplicat al següent Code Sandbox:

.navbar {
    display: flex;
    position: sticky;
    padding: 1rem 0.5rem;
    margin-bottom: 3rem;
    margin-top: 5rem;
    flex-direction: row;
    align-items: flex-start;
    top: 0;
  
    @media (min-width: 768px) {
      padding: 1rem 1.25rem;
    }
  }
  
  .backdrop {
    position: absolute;
    top: 0;
    min-width: 100%;
    height: 5rem;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    left: -8px;
    right: -8px;
  
    @media (min-width: 768px) {
      margin-right: -28px;
      margin-left: -28px;
    }
  }
  
 .navbar .navbar-content {
  transition: transform 0.3s ease;
  will-change: transform;
  transform: translateY(calc(0px - var(--navbar-shrink) * 12px));
}
.navbar .backdrop {
  transition: transform 0.3s ease;
  will-change: transform;
  transform: translateY(calc(0px - var(--navbar-shrink) * 24px));
}

.navbar .logo {
  transition: transform 0.3s ease;
  will-change: transform;
  transform: scale(calc(1 - var(--navbar-shrink) * 0.2));
}

.navbar .site-name > p {
  transition: opacity 0.3s ease;
  opacity: calc(1 - var(--navbar-shrink));
}

@supports (animation-timeline: scroll()) {
  .navbar {
    --navbar-shrink: 1;

    animation: shrink;
    animation-timeline: scroll(nearest block);
    animation-timing-function: ease;
  }
}

@keyframes shrink {
  0% {
    --navbar-shrink: 0;
  }
  10%,
  100% {
    --navbar-shrink: 1;
  }
}
  
.navbar-content {
  display: flex;
  z-index: 10;
  margin-left: 0;
  flex-direction: row;
  justify-content: space-between;
  min-width: 100%;
}

.logo {
  margin-right: 1rem;
}

.logo-link {
  display: flex;
  flex-direction: row;
  align-items: center;
  text-decoration: none;
}

.site-name {
  display: flex;
  flex-direction: column;
}

.site-name > p {
  color: white;
  margin: 0;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;

  @media (min-width: 640px) {
    font-size: 1.5rem;
    line-height: 2rem;
  }
}

.navigation-links-container {
  display: flex;
  flex-direction: row-reverse;
  gap: 1.5rem;

  @media (min-width: 640px) {
    flex-direction: row;
  }
}

.navigation-links {
  display: none;
  flex-direction: row;
  align-items: center;

  @media (min-width: 640px) {
    display: flex;
  }
}

.navigation-links a {
  position: relative;
  padding: 0.25rem 0.5rem;
}

.scrollable-container {
  min-height: 150dvh;
}

body {
  font-family: sans-serif;
  -webkit-font-smoothing: auto;
  -moz-font-smoothing: auto;
  -moz-osx-font-smoothing: grayscale;
  font-smoothing: auto;
  text-rendering: optimizeLegibility;
  font-smooth: always;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;

  background-color: color-mix(in srgb, slategrey 100%, black 100%);
  color: white;
}

Això és tot amics! 🥕

Hem afegit algunes pinzellades de bon gust a la UX per millorar l’experiència dels usuaris que utilitzen dispositius amb pantalles petites aplicant una animació a la barra de navegació que te millor rendiment que la variant tradicional, és més extensible i domés utilitza CSS. També hem afegit una alternativa per aquells usuaris que fan ús de navegadors no compatibles.

T’anim a experimentar amb el Code Sandbox afegint més keyframes, més variables de CSS i en definitiva més variacions.

Esper que haguis gaudit d’aquesta entrada. Fins aviat! 😄