Cómo crear en JavaScript una galería de imágenes que se puedan arrastrar con GSAP
Spanish (Español) translation by Carlos (you can also view the original English article)
En un tutorial anterior, aprendimos a crear una galería de imágenes responsiva con slick.js. Hoy vamos a crear algo similar: una galería de imágenes responsiva con una imagen destacada/diapositiva principal que se puede arrastrar. Para hacer que el elemento objetivo se pueda arrastrar, aprovecharemos el plugin Draggable de GSAP.
¿Te parece un buen ejercicio?
Lo que crearemos
Esta es la galería que vamos a crear:
1. Incluye los plugins necesarios
Como ya se comentó, para hacer que las imágenes destacadas sean elementos que se puedan arrastrar, utilizaremos GSAP y específicamente su plugin Draggable.
De manera opcional, también incluiremos InertiaPlugin (anteriormente ThrowPropsPlugin), un segundo plugin GSAP que aplicará un movimiento basado en el impulso después de que se suelte el mouse/touch. Cabe señalar que este es un plugin premium y tienes que registrarte para conseguir una membresía de GSAP antes de decidir usarlo. En nuestro caso, usaremos una versión de prueba que únicamente funciona de manera local y en dominios como codepen.io (mira la consola del navegador de la demostración para que obtengas más detalles).
Teniendo todo esto en cuenta, incluiremos tres archivos de JavaScript externos. Los dos primeros son obligatorios, mientras que el tercero es opcional.



2. Define el marcado HTML
Definiremos un elemento de envoltura que contendrá dos listas: la lista de las imágenes en miniatura y la lista de las imágenes destacadas. Ambas listas incluirán las mismas imágenes de Unsplash. Estas tendrán dimensiones iguales y serán lo suficientemente grandes como para implementar el efecto «arrastrable».
De manera predeterminada, aparecerá la primera diapositiva principal. Pero podemos configurar ese comportamiento añadiendo la clase is-active a la diapositiva deseada (listas).
Asimismo, todas las imágenes destacadas conservarán sus dimensiones originales (1920 x 1280px).
Esta es la estructura que se requiere para nuestra página de demostración:
1 |
<div class="gallery-wrapper"> |
2 |
<ul class="thumb-list"> |
3 |
<li class="is-active"> |
4 |
<img width="1920" height="1280" src="sports-car1.jpg" alt=""> |
5 |
</li>
|
6 |
<li>
|
7 |
<img width="1920" height="1280" src="sports-car2.jpg" alt=""> |
8 |
</li>
|
9 |
<li>
|
10 |
<img width="1920" height="1280" src="sports-car3.jpg" alt=""> |
11 |
</li>
|
12 |
<li>
|
13 |
<img width="1920" height="1280" src="sports-car4.jpg" alt=""> |
14 |
</li>
|
15 |
</ul>
|
16 |
<ul class="featured-list"> |
17 |
<li class="is-active"> |
18 |
<div class="featured-img" style="background-image: url(sports-car1.jpg); width: 1920px; height: 1280px;"></div> |
19 |
</li>
|
20 |
<li>
|
21 |
<div class="featured-img" style="background-image: url(sports-car2.jpg); width: 1920px; height: 1280px;"></div> |
22 |
</li>
|
23 |
<li>
|
24 |
<div class="featured-img" style="background-image: url(sports-car3.jpg); width: 1920px; height: 1280px;"></div> |
25 |
</li>
|
26 |
<li>
|
27 |
<div class="featured-img" style="background-image: url(sports-car4.jpg); width: 1920px; height: 1280px;"></div> |
28 |
</li>
|
29 |
</ul>
|
30 |
</div>
|
3. Especifica los estilos principales
Con el marcado preparado, continuaremos con los estilos principales de nuestra galería. Para simplificar, omitiré los estilos de introducción/reinicio. También no optimizaré ni fusionaré los estilos CSS comunes, por lo que te resultará más fácil entender lo que está sucediendo. Asegúrate de verlos todos haciendo clic en la pestaña CSS de la demostración.
Establece el diseño de la galería
La galería tendrá un ancho máximo de 950px.
En pantallas grandes (>750 px), tendremos dos columnas. Las miniaturas aparecerán en el lado izquierdo, mientras que las imágenes destacadas estarán en el derecho, de esta forma:



Observa que las miniaturas cubrirán una cuarta parte del ancho de la galería, mientras que las imágenes destacadas cubrirán tres cuartas partes.
En pantallas pequeñas (≤750px), las miniaturas estarán situadas debajo de la imagen destacada, así:



Observa que cada miniatura cubrirá un cuarto del ancho del elemento parent.
Estos son los estilos asociados:
1 |
.gallery-wrapper { |
2 |
max-width: 950px; |
3 |
padding: 0 15px; |
4 |
margin: 0 auto; |
5 |
display: grid; |
6 |
grid-template-columns: 1fr 3fr; |
7 |
grid-gap: 15px; |
8 |
}
|
9 |
|
10 |
.gallery-wrapper .thumb-list { |
11 |
display: grid; |
12 |
grid-gap: 15px; |
13 |
}
|
14 |
|
15 |
@media (max-width: 750px) { |
16 |
.gallery-wrapper { |
17 |
grid-template-columns: 1fr; |
18 |
}
|
19 |
|
20 |
.gallery-wrapper .thumb-list { |
21 |
grid-template-columns: repeat(4, 1fr); |
22 |
order: 1; |
23 |
}
|
24 |
}
|
Visibilidad de diapositivas destacadas
Por defecto, todas las diapositivas destacadas estarán ocultas, excepto la diapositiva activa. Además, solamente aparecerá una diapositiva destacada (la activa) a la vez.
Estos son los estilos asociados:
1 |
.gallery-wrapper .featured-list li { |
2 |
opacity: 0; |
3 |
transition: opacity 0.25s; |
4 |
}
|
5 |
|
6 |
.gallery-wrapper .featured-list li.is-active { |
7 |
opacity: 1; |
8 |
}
|
Posición de las imágenes destacadas
En pantallas grandes, las dos columnas de la galería tendrán la misma altura que los elementos de la cuadrícula. No obstante, las imágenes destacadas serán elementos posicionados de manera absoluta y centrados dentro de su contenedor. Para ver todas sus partes tenemos que arrastrarlas.
En pantallas pequeñas, ya que las columnas están apiladas y las imágenes destacadas todavía están posicionadas de manera absoluta, debemos especificar una altura fija para la columna de la derecha.
Estos son los estilos asociados:
1 |
.gallery-wrapper .featured-list { |
2 |
position: relative; |
3 |
overflow: hidden; |
4 |
}
|
5 |
|
6 |
.gallery-wrapper .featured-list .featured-img { |
7 |
background-size: cover; |
8 |
background-repeat: no-repeat; |
9 |
background-position: center; |
10 |
z-index: 1 !important; |
11 |
position: absolute; |
12 |
top: 50%; |
13 |
left: 50%; |
14 |
transform: translate(-50%, -50%); |
15 |
}
|
16 |
|
17 |
@media (max-width: 750px) { |
18 |
.gallery-wrapper .featured-list { |
19 |
height: 340px; |
20 |
}
|
21 |
}
|
Indica el estado active y hover
Cada vez que pasemos el puntero del mouse sobre una miniatura, aparecerá su pseudoelemento ::before. Este tendrá un fondo azul claro y se colocará en la parte superior de la miniatura.
Por otra parte, la miniatura activa recibirá un borde de color rojo.

Estos son los estilos asociados:
1 |
/*CUSTOM VARIABLES HERE*/ |
2 |
|
3 |
.gallery-wrapper .thumb-list li {
|
4 |
position: relative; |
5 |
cursor: pointer; |
6 |
border: 4px solid var(--black); |
7 |
} |
8 |
|
9 |
.gallery-wrapper .thumb-list li:not(.is-active):hover::before {
|
10 |
content: ""; |
11 |
position: absolute; |
12 |
top: 0; |
13 |
left: 0; |
14 |
right: 0; |
15 |
bottom: 0; |
16 |
background: var(--hovered-thumb); |
17 |
} |
18 |
|
19 |
.gallery-wrapper .thumb-list li.is-active {
|
20 |
border-color: var(--red); |
21 |
} |
4. Añade el código JavaScript
¡Ahora vamos a darle vida a nuestra galería!
Cambiar las diapositivas
Cada vez que hagamos clic en una miniatura, realizaremos las siguientes acciones:
- Eliminaremos la clase
is-activede la miniatura activa preexistente y de la imagen destacada. - Buscaremos el índice de la miniatura activa actual.
- Asignaremos la clase
is-activea la miniatura activa y a la imagen destacada cuyo índice coincide con el índice de esta miniatura.



Este es el código que se requiere:
1 |
const thumbList = document.querySelector(".thumb-list"); |
2 |
const thumbItems = thumbList.querySelectorAll("li"); |
3 |
const featuredList = document.querySelector(".featured-list"); |
4 |
const isActiveClass = "is-active"; |
5 |
|
6 |
thumbItems.forEach(function (el) { |
7 |
el.addEventListener("click", function () { |
8 |
thumbList.querySelector("li.is-active").classList.remove(isActiveClass); |
9 |
featuredList.querySelector("li.is-active").classList.remove(isActiveClass); |
10 |
let index = Array.from(thumbItems).indexOf(el); |
11 |
el.classList.add(isActiveClass); |
12 |
featuredList
|
13 |
.querySelector(`li:nth-child(${++index})`) |
14 |
.classList.add(isActiveClass); |
15 |
});
|
16 |
});
|
Añade soporte del teclado
Ahora mejoremos la experiencia del usuario brindando soporte para la navegación mediante el teclado. De manera más específica:
- Cada vez que se presionen las teclas flecha arriba (↑) o flecha abajo (↓), recuperaremos la miniatura activa preexistente.
- Si se presiona la tecla flecha arriba, la miniatura que viene antes de la miniatura actual se activará. En caso de que no exista tal miniatura, la última miniatura se activará.
- Si se presiona la tecla flecha abajo, la miniatura que viene después de la miniatura actual se activará. En caso de que no exista tal miniatura, la primera miniatura se activará.
Este es el código que se necesita
1 |
...
|
2 |
|
3 |
document.addEventListener("keyup", (e) => { |
4 |
if (e.keyCode === 38 || e.keyCode === 40) { |
5 |
const activeThumb = thumbList.querySelector("li.is-active"); |
6 |
// up arrow
|
7 |
if (e.keyCode === 38) { |
8 |
if (activeThumb.previousElementSibling) { |
9 |
activeThumb.previousElementSibling.click(); |
10 |
} else { |
11 |
thumbList.lastElementChild.click(); |
12 |
}
|
13 |
} else { |
14 |
// down arrow
|
15 |
if (activeThumb.nextElementSibling) { |
16 |
activeThumb.nextElementSibling.click(); |
17 |
} else { |
18 |
thumbList.firstElementChild.click(); |
19 |
}
|
20 |
}
|
21 |
}
|
22 |
});
|
Haz que las imágenes destacadas se puedan arrastrar
En este último paso, haremos que las imágenes destacadas sean elementos que se puedan arrastrar. Para hacer esto, utilizaremos el método create () que recibirá los dos siguientes argumentos:
- Los elementos que queremos arrastrar.
- Un objeto de configuración. Dentro de él, especificaremos los límites en los que los elementos arrastrables deben permanecer durante el efecto. De manera opcional, como hemos cargado InertiaPlugin, también solicitaremos el movimiento basado en el impulso de la propiedad
inertiadespués de que los usuarios suelten el mouse/touch.
Este es el código correspondiente:
1 |
const featuredList = document.querySelector(".featured-list"); |
2 |
const featuredImgs = featuredList.querySelectorAll(".featured-img"); |
3 |
|
4 |
Draggable.create(featuredImgs, { |
5 |
bounds: featuredList, |
6 |
inertia: true |
7 |
});
|
Por supuesto, aquí tratamos únicamente la parte básica de la funcionalidad del plugin. Puedes profundizar aún más leyendo los documentos e implementando cosas complejas.
Conclusión
¡Otro ejercicio ha llegado a su fin, amigos! Gracias por seguirlo. Esperemos que hayan disfrutado de lo que creamos hoy y que les haya brindado un conocimiento sólido sobre cómo combinar código personalizado con el poder de plugins populares como GSAP.
Aquí tienes un recordatorio de lo que hemos creado:
Por último, aunque no menos importante, recuerda que GSAP no es la única manera de crear un efecto «arrastrable». Te invitamos a que pruebes otra opción y que la compartas con nosotros.
Además, si deseas practicar con esta demostración, tengo un reto para ti: crea un efecto lighbox personalizado que se abra cada vez que hagas clic en el botón correspondiente. Mira el botón de llamada a la acción a continuación:



Si aceptas el reto, puedes usar el modal que creamos hace algún tiempo como punto de partida. En un próximo tutorial, ofreceré una posible solución. ¡Mantente atento!
Como siempre, ¡muchas gracias por leer!



