⏱️ Lectura: 11 min
Las transiciones de página son ese detalle que separa un sitio web profesional de uno genérico. Cuando navegas y el contenido se transforma fluidamente en vez de parpadear con un reload completo, la experiencia se siente premium.
📑 En este artículo
El problema es que implementarlas siempre fue complicado — SPAs con React/Next.js, routers custom, código frágil. Pero con la combinación de Astro + Barba.js + GSAP, puedes lograr transiciones cinematográficas en un sitio estático sin la complejidad de un SPA.
En este artículo vas a aprender tres técnicas de transición profesional, de menor a mayor complejidad, con código funcional que puedes adaptar a tu proyecto.
El stack y por qué funciona
Cada herramienta cumple un rol específico:
- Astro — genera páginas HTML estáticas ultrarrápidas. Sin JavaScript innecesario en el cliente
- Barba.js — intercepta los clics en links, descarga la siguiente página en background vía fetch, y te da hooks para animar la salida y entrada
- GSAP — la librería de animación más robusta de la web. Maneja timelines, easings y propiedades CSS con precisión de 60fps
💡 Cómo funciona Barba.js: Cuando el usuario hace clic en un link, Barba.js no recarga la página. En su lugar: (1) ejecuta la animación de salida, (2) descarga el HTML de la nueva página con fetch, (3) reemplaza el contenido del container, (4) ejecuta la animación de entrada. Todo sin perder el estado del header, footer o scripts globales.
Setup del proyecto
Crear el proyecto Astro
# Crear proyecto nuevo
npm create astro@latest mi-sitio-transiciones
# Entrar al directorio
cd mi-sitio-transiciones
# Instalar dependencias
npm install @barba/core gsap
Estructura base
Astro usa layouts compartidos. Creamos un layout base que incluya Barba.js y GSAP:
<!-- src/layouts/Base.astro -->
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{Astro.props.title}</title>
</head>
<body>
<nav>
<a href="/">Inicio</a>
<a href="/about">Acerca</a>
<a href="/projects">Proyectos</a>
</nav>
<!-- Barba.js wrapper: envuelve todo el contenido dinámico -->
<main data-barba="wrapper">
<div data-barba="container" data-barba-namespace={Astro.props.page}>
<slot />
</div>
</main>
<footer>Mi sitio © 2026</footer>
<script>
import barba from '@barba/core';
import { initTransitions } from '../scripts/transitions.js';
initTransitions();
</script>
</body>
</html>
Los atributos clave:
data-barba="wrapper"— el contenedor padre que no cambia entre páginasdata-barba="container"— el contenido que sí se reemplaza en cada navegacióndata-barba-namespace— identifica cada página para aplicar transiciones específicas
Páginas de ejemplo
<!-- src/pages/index.astro -->
---
import Base from '../layouts/Base.astro';
---
<Base title="Inicio" page="home">
<section class="hero">
<h1>Bienvenido</h1>
<p>Un sitio con transiciones profesionales.</p>
</section>
</Base>
<!-- src/pages/about.astro -->
---
import Base from '../layouts/Base.astro';
---
<Base title="Acerca" page="about">
<section class="about">
<h1>Sobre nosotros</h1>
<p>Creamos experiencias web memorables.</p>
</section>
</Base>
Ciclo de vida de Barba.js
Antes de las transiciones, necesitas entender los hooks de Barba.js. Cuando el usuario navega, se ejecutan en este orden:
beforeLeave— antes de que empiece la animación de salidaleave— animación de salida (obligatorio retornar una Promise o usardone())afterLeave— la página actual se removió del DOMbeforeEnter— antes de que aparezca el contenido nuevoenter— animación de entradaafterEnter— transición completa
También existe el modo sync (sync: true), donde ambos containers (actual y nuevo) están en el DOM al mismo tiempo. Esto permite animaciones donde la página vieja y la nueva se mueven juntas.
Transición 1: Clip-Path Reveal
La más elegante y la más sencilla. Un círculo se expande desde el centro hasta revelar la nueva página.
// src/scripts/transitions.js
import barba from '@barba/core';
import gsap from 'gsap';
export function initTransitions() {
barba.init({
transitions: [
{
name: 'clip-reveal',
leave(data) {
// Animar la página actual: escalar hacia abajo y desvanecer
return gsap.to(data.current.container, {
scale: 0.95,
opacity: 0,
duration: 0.5,
ease: 'power2.inOut',
});
},
enter(data) {
// La nueva página empieza recortada con clip-path circular
gsap.set(data.next.container, {
clipPath: 'circle(0% at 50% 50%)',
opacity: 1,
});
// Expandir el círculo hasta revelar todo
return gsap.to(data.next.container, {
clipPath: 'circle(150% at 50% 50%)',
duration: 0.8,
ease: 'power3.inOut',
});
},
afterEnter(data) {
// Limpiar estilos inline para no interferir con la página
gsap.set(data.next.container, { clearProps: 'all' });
},
},
],
});
}
El truco está en clip-path: circle(). GSAP puede interpolar valores de clip-path nativamente, así que el círculo se anima de 0% a 150% (más del 100% para asegurar que cubra las esquinas).
💡 Tip: Puedes cambiar el origen del círculo para que se expanda desde donde el usuario hizo clic. Captura las coordenadas del evento click y pásalas al at X% Y% del clip-path.
Variante: origen dinámico desde el clic
let clickX = 50, clickY = 50;
// Capturar posición del clic en los links
document.addEventListener('click', (e) => {
if (e.target.closest('a[href]')) {
clickX = (e.clientX / window.innerWidth) * 100;
clickY = (e.clientY / window.innerHeight) * 100;
}
});
// En el hook enter:
gsap.set(data.next.container, {
clipPath: `circle(0% at ${clickX}% ${clickY}%)`,
});
gsap.to(data.next.container, {
clipPath: `circle(150% at ${clickX}% ${clickY}%)`,
duration: 0.8,
ease: 'power3.inOut',
});
Ahora el reveal se origina exactamente donde el usuario hizo clic — un detalle sutil pero que se siente muy natural.
Transición 2: Overlay con Texto
Un overlay de color se expande desde el centro, muestra el nombre de la siguiente página, y luego se retira revelando el contenido nuevo. Inspirada en sitios como bloomparis.tv.
HTML del overlay
Agregamos un elemento fijo al layout que Barba.js no toca:
<!-- Agregar dentro de Base.astro, fuera del wrapper -->
<div class="transition-overlay" aria-hidden="true">
<span class="transition-overlay__text"></span>
</div>
CSS del overlay
.transition-overlay {
position: fixed;
inset: 0;
z-index: 9999;
background: #1e293b;
display: flex;
align-items: center;
justify-content: center;
clip-path: circle(0% at 50% 50%);
pointer-events: none;
}
.transition-overlay__text {
color: white;
font-size: 2rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
opacity: 0;
}
JavaScript de la transición
{
name: 'overlay-text',
leave(data) {
const overlay = document.querySelector('.transition-overlay');
const text = overlay.querySelector('.transition-overlay__text');
// Extraer nombre de la página destino desde la URL
const nextPage = data.next.url.path.replace(/\//g, '') || 'inicio';
text.textContent = nextPage;
const tl = gsap.timeline();
// 1. Expandir overlay
tl.to(overlay, {
clipPath: 'circle(150% at 50% 50%)',
duration: 0.6,
ease: 'power3.inOut',
});
// 2. Mostrar texto
tl.to(text, {
opacity: 1,
y: 0,
duration: 0.3,
ease: 'power2.out',
}, '-=0.2');
// 3. Pausa para que el usuario lea
tl.to({}, { duration: 0.4 });
// 4. Ocultar texto
tl.to(text, {
opacity: 0,
y: -20,
duration: 0.3,
ease: 'power2.in',
});
return tl;
},
enter(data) {
const overlay = document.querySelector('.transition-overlay');
// Contraer overlay revelando la nueva página
return gsap.to(overlay, {
clipPath: 'circle(0% at 50% 50%)',
duration: 0.6,
ease: 'power3.inOut',
});
},
afterEnter() {
// Reset para la próxima transición
const overlay = document.querySelector('.transition-overlay');
gsap.set(overlay, { clipPath: 'circle(0% at 50% 50%)' });
},
}
El timeline de GSAP orquesta toda la secuencia: overlay se expande → texto aparece → pausa → texto desaparece → overlay se contrae. Cada paso se encadena automáticamente.
Transición 3: Cortina con Pseudo-Elemento
Una “cortina” que baja desde arriba cubriendo la página actual, y luego sube revelando la nueva. Usa un pseudo-elemento CSS controlado con una variable custom.
CSS de la cortina
.transition-curtain::before {
content: '';
position: fixed;
inset: 0;
z-index: 9998;
background: #0f172a;
transform: translateY(calc(var(--curtain-progress, -100) * 1%));
pointer-events: none;
will-change: transform;
}
/* Bloquear interacciones durante la transición */
body.is-transitioning {
pointer-events: none;
}
body.is-transitioning * {
cursor: wait;
}
La variable --curtain-progress controla la posición: -100 = fuera de pantalla arriba, 0 = cubriendo todo, 100 = fuera de pantalla abajo.
JavaScript
{
name: 'curtain',
leave(data) {
document.body.classList.add('is-transitioning');
// La cortina baja cubriendo la página
return gsap.fromTo(document.documentElement,
{ '--curtain-progress': -100 },
{
'--curtain-progress': 0,
duration: 0.5,
ease: 'power2.inOut',
}
);
},
enter(data) {
// Scroll al inicio de la nueva página
window.scrollTo(0, 0);
// La cortina sigue bajando y sale por abajo
return gsap.fromTo(document.documentElement,
{ '--curtain-progress': 0 },
{
'--curtain-progress': 100,
duration: 0.5,
ease: 'power2.inOut',
onComplete: () => {
document.body.classList.remove('is-transitioning');
gsap.set(document.documentElement, {
'--curtain-progress': -100,
});
},
}
);
},
}
📝 Nota: Animar una CSS custom property con GSAP funciona porque GSAP trata--curtain-progresscomo un número e interpola el valor. El pseudo-elemento::beforelee la variable en tiempo real víacalc(). Es más eficiente que animartransformdirectamente con JavaScript porque el navegador optimiza pseudo-elementos en el compositor.
Buenas prácticas
Después de implementar cualquiera de estas transiciones, ten en cuenta estos puntos:
- Bloquea interacciones — agrega una clase como
is-transitioningal body durante la animación para evitar clics múltiples que rompan el estado - Limpia estilos inline — usa
gsap.set(el, { clearProps: 'all' })en elafterEnterpara que GSAP no deje propiedades residuales - Respeta
prefers-reduced-motion— si el usuario tiene animaciones reducidas, usa transiciones instantáneas o solo un fade simple - Scroll position — llama
window.scrollTo(0, 0)en elenterpara que la nueva página empiece desde arriba - Scripts de la nueva página — Barba.js no re-ejecuta scripts del
<head>. Usa los hooksafterEnterpara reinicializar componentes que dependan del DOM
// Respetar prefers-reduced-motion
const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
barba.init({
transitions: [
{
leave(data) {
if (prefersReduced) {
return gsap.to(data.current.container, {
opacity: 0,
duration: 0.15,
});
}
// ... transición completa
},
},
],
});
Transiciones condicionales por página
Barba.js permite aplicar transiciones diferentes según la página de origen o destino. Usa las propiedades from y to con los namespaces:
barba.init({
transitions: [
{
name: 'home-to-projects',
from: { namespace: ['home'] },
to: { namespace: ['projects'] },
leave(data) { /* clip-path reveal */ },
enter(data) { /* ... */ },
},
{
name: 'projects-to-detail',
from: { namespace: ['projects'] },
to: { namespace: ['project-detail'] },
leave(data) { /* overlay con texto */ },
enter(data) { /* ... */ },
},
{
name: 'default',
leave(data) { /* cortina como fallback */ },
enter(data) { /* ... */ },
},
],
});
Barba.js evalúa las transiciones de arriba hacia abajo y usa la primera que coincida. La última sin from/to actúa como fallback para cualquier navegación no cubierta.
Instalación paso a paso
Resumen rápido para los tres sistemas operativos:
Windows (PowerShell)
npm create astro@latest mi-sitio
cd mi-sitio
npm install @barba/core gsap
npm run dev
macOS (Terminal)
npm create astro@latest mi-sitio
cd mi-sitio
npm install @barba/core gsap
npm run dev
Linux
npm create astro@latest mi-sitio
cd mi-sitio
npm install @barba/core gsap
npm run dev
💡 Tip: Si usas Bun como runtime, reemplazanpmporbunen todos los comandos. Bun instala dependencias hasta 25x más rápido.
Preguntas frecuentes
¿Barba.js funciona con cualquier framework?
Sí. Barba.js es agnóstico al framework — funciona con Astro, Hugo, Eleventy, Jekyll, WordPress, o cualquier sitio que genere HTML con links <a> normales. No es compatible con SPAs como Next.js o Nuxt porque estos ya manejan su propia navegación client-side.
¿GSAP es gratis?
El core de GSAP es gratis y open source para uso comercial. Los plugins premium (SplitText, MorphSVG, DrawSVG) requieren una membresía GSAP Club. Las tres transiciones de este artículo usan solo el core gratuito.
¿Afecta al SEO?
No. Astro genera HTML estático que los crawlers indexan normalmente. Barba.js solo actúa en el lado del cliente después de que la página se ha cargado. Los bots de Google no ejecutan las transiciones — ven HTML puro.
¿Puedo usar esto con WordPress?
Sí. Barba.js funciona con cualquier sitio que use links HTML estándar. Solo necesitas agregar los atributos data-barba al markup de tu tema y cargar los scripts. La lógica es la misma.
Referencias
- Documentación oficial de Barba.js
- Documentación de GSAP v3
- Guía de inicio de Astro (en español)
- Creating Custom Page Transitions in Astro — Codrops (artículo original en inglés)
- MDN — clip-path
Síguenos en t.me/programacion para más contenido tech diario.
0 Comentarios