Las guías de estilo que Google estrenó en 2014 bajo el nombre de Material Design han sido muy aplaudidas por su simplicidad. Parte del éxito recae sobre las fluidas animaciones de las que hace gala sus aplicaciones.
En esta entrada quiero compartir una ligera recreación que hice usando JavaScript puro.
La estructura HTML
El HTML es prácticamente igual al de un sistema de tabs normal:
<nav class="tabs">
<ul class="tabs-list" id="tabs">
<li class="tab-item">Tab 1</li>
<li class="tab-item">Tab 2</li>
<li class="tab-item">Tab 3</li>
<li class="tab-item">Tab 4</li>
<li class="tab-item">Tab 5</li>
</ul>
<div class="highlighter" id="highlighter"></div>
</nav>
Por un lado está el contenedor principal nav
que envuelve el componente:
- La lista
ul
con todos los ítems del menú. - Un
div
con la clasehighlighter
al que se le aplicarán estilos para convertirlo en la barra inferior que se mueve por los diferentes ítems según vayan siendo seleccionados.
Añadiendo estilos
A través de los estilos se le dará la apariencia tipo Material Design:
:root {
--num-tabs: 0; /* Lo modificaremos con JS */
}
.tabs {
position: relative;
width: 640px; /* También puede ser un porcentaje como 100% */
}
.tabs-list {
display: inline-block;
width: 100%;
}
.tab-item {
align-items: center;
cursor: pointer;
display: flex;
float: left;
height: 60px;
justify-content: center;
transition: background-color 0.2s ease;
width: calc(100% / var(--num-tabs));
}
.tab-item:hover {
background-color: rgba(238, 110, 115, 0.2);
}
.highlighter {
background-color: #EE6E73;
bottom: 0;
height: 4px;
position: absolute;
transition: transform 0.2s ease-out;
width: calc(100% / var(--num-tabs));
}
Para calcular el ancho de cada ítem se utiliza la variable CSS que será editada vía JavaScript con el número de tabs. La magia de la animación reside completamente en el highlighter, que se moverá entre pestañas gracias a la propiedad transform: translateX()
. Este valor también vendrá definido en el código JavaScript.
La animación con JavaScript
Ya solo faltaría animar el div
con la clase highlighter
utilizando la propiedad transform
.
Ya que el elemento está posicionado respecto a las coordenadas del padre también podría usarse
left
. Sin embargo,transform
no modifica la geometría de la página, lo que se traduce en animaciones más fluidas. En esta página puedes ver las operaciones que hace cada motor para renderizar una propiedad.
const tabs = Array.prototype.slice.apply(document.querySelectorAll('.tab-item'));
const tabContainer = document.getElementById('tabs');
const tabHighlighter = document.getElementById('highlighter');
const tabsWidth = tabContainer.offsetWidth;
document.documentElement.style.setProperty('--num-tabs', tabs.length);
tabContainer.addEventListener('click', e => {
if (e.target.classList.contains('tab-item')) {
let item = tabs.indexOf(e.target);
tabs.map(tab => tab.classList.remove('active'));
tabs[item].classList.add('active');
tabHighlighter.style.transform = `translateX(${(tabsWidth / tabs.length) * item }px)`;
}
});
El código es realmente sencillo, ya que su única función es calcular en que posición deberá encontrarse el highlighter. En primer lugar, se definen las variables que intervendrán en el cálculo:
- Un array con todos los ítems, que además modificará el valor de la variable CSS
--num-tabs
(definida en los estilos). - El contenedor
nav
que engloba todas las tabs y se usará para escuchar los clics. - El ancho en píxeles de todo el contenedor
nav
. - Y el elemento estrella de la función: el
highligter
.
Dado que tenemos escuchar los clics de cada uno de los ítems, una opción inteligente sería escuchar el clic del elemento padre (nav
), y detectar el índice del ítem accionado. Ese dato lo podemos encontrar en el e.target
del evento.
Ya solo faltaría calcular cuántos píxeles hay que mover el highligter. Para ello simplemente se multiplica el índice del ítem por el tamaño de cada tab, que no será más que el ancho total del contenedor dividido por el número de ítems (tabsWidth / tabs.length
).