Cómo recrear el efecto dominó de los botones de Material Design
Cuando descubrí Material Design por primera vez, me inspiré particularmente en su componente de botón. Utiliza un efecto dominó para brindar comentarios a los usuarios de una manera simple y elegante.
¿Cómo funciona este efecto? Los botones de Material Design no solo presentan una animación ondulada ordenada, sino que la animación también cambia de posición dependiendo de dónde se hace clic en cada botón.
Podemos lograr el mismo resultado. Comenzaremos con una solución concisa utilizando ES6+ JavaScript, antes de ver algunos enfoques alternativos.
HTML
Nuestro objetivo es evitar cualquier marcado HTML superfluo. Así que iremos con lo mínimo indispensable:
buttonFind out more/button
Aplicar estilo al botón
Necesitaremos diseñar algunos elementos de nuestra onda dinámicamente, usando JavaScript. Pero todo lo demás se puede hacer en CSS. Para nuestros botones sólo es necesario incluir dos propiedades.
button { position: relative; overflow: hidden;}
El uso position: relative
nos permite utilizar position: absolute
nuestro elemento ondulado, que necesitamos para controlar su posición. Mientras tanto, overflow: hidden
evita que la ondulación exceda los bordes del botón. Todo lo demás es opcional. Pero ahora mismo, nuestro botón parece un poco anticuado. Aquí hay un punto de partida más moderno:
/* Roboto is Material's default font */@import url('https://fonts.googleapis.com/css2?family=Robotodisplay=swap');button { position: relative; overflow: hidden; transition: background 400ms; color: #fff; background-color: #6200ee; padding: 1rem 2rem; font-family: 'Roboto', sans-serif; font-size: 1.5rem; outline: 0; border: 0; border-radius: 0.25rem; box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3); cursor: pointer;}
Diseñando las ondas
Más adelante, usaremos JavaScript para inyectar ondas en nuestro HTML como intervalos con una .ripple
clase. Pero antes de pasar a JavaScript, definamos un estilo para esas ondas en CSS para tenerlas listas:
span.ripple { position: absolute; /* The absolute position we mentioned earlier */ border-radius: 50%; transform: scale(0); animation: ripple 600ms linear; background-color: rgba(255, 255, 255, 0.7);}
Para que nuestras ondas sean circulares, hemos establecido el valor border-radius
en 50%. Y para garantizar que cada onda surja de la nada, hemos establecido la escala predeterminada en 0. En este momento, no podremos ver nada porque todavía no tenemos un valor para las propiedades top
, left
, width
o height
; Pronto inyectaremos estas propiedades con JavaScript.
En cuanto a nuestro CSS, lo último que debemos agregar es un estado final para la animación:
@keyframes ripple { to { transform: scale(4); opacity: 0; }}
¿Observa que no estamos definiendo un estado inicial con la from
palabra clave en los fotogramas clave? Podemos omitir from
y CSS construirá los valores faltantes en función de los que se aplican al elemento animado. Esto ocurre si los valores relevantes se establecen explícitamente (como en transform: scale(0)
) o si son los predeterminados, como opacity: 1
.
Ahora para el JavaScript
Finalmente, necesitamos JavaScript para establecer dinámicamente la posición y el tamaño de nuestras ondas. El tamaño debe basarse en el tamaño del botón, mientras que la posición debe basarse tanto en la posición del botón como en la del cursor.
Comenzaremos con una función vacía que toma un evento de clic como argumento:
function createRipple(event) { //}
Accederemos a nuestro botón buscando el currentTarget
del evento.
const button = event.currentTarget;
A continuación, crearemos una instancia de nuestro elemento span y calcularemos su diámetro y radio en función del ancho y alto del botón.
const circle = document.createElement("span");const diameter = Math.max(button.clientWidth, button.clientHeight);const radius = diameter / 2;
Ahora podemos definir las propiedades restantes que necesitamos para nuestras ondas : left
, y .top
width
height
circle.style.width = circle.style.height = `${diameter}px`;circle.style.left = `${event.clientX - (button.offsetLeft + radius)}px`;circle.style.top = `${event.clientY - (button.offsetTop + radius)}px`;circle.classList.add("ripple");
Antes de agregar nuestro elemento span al DOM, es una buena práctica verificar si hay ondulaciones existentes que puedan quedar de clics anteriores y eliminarlas antes de ejecutar el siguiente.
const ripple = button.getElementsByClassName("ripple")[0];if (ripple) { ripple.remove();}
Como paso final, agregamos el intervalo como elemento secundario al elemento del botón para que se inyecte dentro del botón.
button.appendChild(circle);
Con nuestra función completa, todo lo que queda es llamarla. Esto podría hacerse de varias maneras. Si queremos agregar la onda a cada botón de nuestra página, podemos usar algo como esto:
const buttons = document.getElementsByTagName("button");for (const button of buttons) { button.addEventListener("click", createRipple);}
¡Ahora tenemos un efecto dominó que funciona!
Llevándolo más lejos
¿Y si queremos ir más allá y combinar este efecto con otros cambios en la posición o tamaño de nuestro botón? La posibilidad de personalizar es, al fin y al cabo, una de las principales ventajas que tenemos al optar por recrear el efecto nosotros mismos. Para probar qué tan fácil es extender nuestra función, decidí agregar un efecto de "imán", que hace que nuestro botón se mueva hacia nuestro cursor cuando el cursor está dentro de un área determinada.
Necesitamos confiar en algunas de las mismas variables definidas en la función de ondulación. En lugar de repetir el código innecesariamente, deberíamos almacenarlo en algún lugar al que puedan acceder ambos métodos. Pero también debemos mantener las variables compartidas en el ámbito de cada botón individual. Una forma de lograr esto es mediante el uso de clases, como en el siguiente ejemplo:
Dado que el efecto magnético necesita realizar un seguimiento del cursor cada vez que se mueve, ya no necesitamos calcular la posición del cursor para crear una onda. En cambio, podemos confiar en cursorX
y cursorY
.
Dos nuevas variables importantes son magneticPullX
y magneticPullY
. Controlan la fuerza con la que nuestro método magnético tira del botón detrás del cursor. Entonces, cuando definimos el centro de nuestra onda, debemos ajustar tanto la posición del nuevo botón ( x
y y
) como la atracción magnética.
const offsetLeft = this.left + this.x * this.magneticPullX;const offsetTop = this.top + this.y * this.magneticPullY;
Para aplicar estos efectos combinados a todos nuestros botones, necesitamos crear una nueva instancia de la clase para cada uno:
const buttons = document.getElementsByTagName("button");for (const button of buttons) { new Button(button);}
Otras técnicas
Por supuesto, esta es sólo una forma de lograr un efecto dominó. En CodePen, hay muchos ejemplos que muestran diferentes implementaciones. A continuación se muestran algunos de mis favoritos.
Solo CSS
Si un usuario ha desactivado JavaScript, nuestro efecto dominó no tiene ninguna alternativa. Pero es posible acercarse al efecto original solo con CSS, utilizando la pseudoclase :active para responder a los clics. La principal limitación es que la onda sólo puede surgir de un lugar (normalmente el centro del botón) en lugar de responder a la posición de nuestros clics. Este ejemplo de Ben Szabo es particularmente conciso:
JavaScript anterior a ES6
La demostración de Leandro Parice es similar a nuestra implementación pero es compatible con versiones anteriores de JavaScript:
jQuery
Este ejemplo utiliza jQuery para lograr el efecto dominó. Si ya tienes jQuery como dependencia, podría ayudarte a ahorrar algunas líneas de código.
Reaccionar
Finalmente, un último ejemplo mío. Aunque es posible utilizar funciones de React como estado y referencias para ayudar a crear el efecto dominó, no son estrictamente necesarias. La posición y el tamaño de la onda deben calcularse para cada clic, por lo que no hay ninguna ventaja en mantener esa información en estado. Además, podemos acceder a nuestro elemento de botón desde el evento de clic, por lo que tampoco necesitamos referencias.
Este ejemplo de React utiliza una createRipple
función idéntica a la de la primera implementación de este artículo. La principal diferencia es que, como método del Button
componente, nuestra función tiene como alcance ese componente. Además, el onClick
detector de eventos ahora forma parte de nuestro JSX:
Deja un comentario