Animaciones gsap
Patrones de animación GSAP + ScrollTrigger en Vanilla JS para landing pages. Usa este skill cuando el usuario pida animaciones, transiciones, efectos al scroll, o interacciones avanzadas en una landing page. Incluye navbar con cambio de estado, fade-up, split text, parallax, typewriter, acordeón, baraja de cards y más.From its SKILL.md
npx -y skills add magallon/website-audit-toolkit --skill animaciones-gsapAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
11.8 KB, ~3.2k tokens by cl100k_base, as published. Nobody here has run it
Skill: Animaciones GSAP para Landing Pages (Vanilla JS)
Reglas Generales de Animación
- Siempre carga GSAP y ScrollTrigger vía CDN al final del body con
defer. - Todo el código de animación va en
app.js. Nunca en archivos separados. - Usa
gsap.context()para agrupar animaciones y facilitar cleanup. - Registra ScrollTrigger una sola vez al inicio:
gsap.registerPlugin(ScrollTrigger); - Todas las animaciones deben ser sobrias. Duración mínima 0.4s. Ease recomendado:
power2.outopower3.out. - Nunca uses animaciones "infantiles" (bounce exagerado, colores parpadeantes, zoom agresivo).
- Respeta
prefers-reduced-motion: desactiva animaciones si el usuario lo pide.
Estructura base de app.js
document.addEventListener('DOMContentLoaded', () => {
gsap.registerPlugin(ScrollTrigger);
const ctx = gsap.context(() => {
// Aquí van todas las animaciones
initNavbar();
initHeroAnimations();
initScrollAnimations();
initInteractions();
});
// Cleanup si se necesita
// ctx.revert();
});
Verificar prefers-reduced-motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
// Inicializar animaciones
}
Patrón 1: Navbar con cambio de estado al scroll
Navbar tipo píldora flotante que cambia de transparente a sólida al hacer scroll.
function initNavbar() {
const navbar = document.querySelector('.navbar');
ScrollTrigger.create({
start: 'top -80',
onUpdate: (self) => {
if (self.direction === 1 && self.scroll() > 80) {
navbar.classList.add('navbar-scrolled');
}
if (self.scroll() < 80) {
navbar.classList.remove('navbar-scrolled');
}
}
});
}
CSS necesario en styles.css:
.navbar {
position: fixed;
top: 1rem;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
padding: 0.75rem 2rem;
border-radius: 9999px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
background: transparent;
}
.navbar-scrolled {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(255, 255, 255, 0.3);
}
Patrón 2: Fade-up escalonado (Hero)
Aparición escalonada de elementos del hero de abajo hacia arriba.
function initHeroAnimations() {
const heroElements = gsap.utils.toArray('.hero-animate');
gsap.from(heroElements, {
y: 60,
opacity: 0,
duration: 0.8,
stagger: 0.15,
ease: 'power3.out',
delay: 0.3
});
}
HTML: añadir clase hero-animate a cada elemento que debe aparecer en secuencia.
<h1 class="hero-animate">Titular principal</h1>
<p class="hero-animate">Subtítulo descriptivo</p>
<div class="hero-animate">
<a href="#" class="btn-cta">CTA Principal</a>
<a href="#" class="btn-secondary">CTA Secundario</a>
</div>
Patrón 3: Aparición al scroll (Secciones generales)
Elementos que aparecen suavemente al entrar en el viewport.
function initScrollAnimations() {
// Fade up general
gsap.utils.toArray('.animate-on-scroll').forEach(el => {
gsap.from(el, {
y: 40,
opacity: 0,
duration: 0.7,
ease: 'power2.out',
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none'
}
});
});
// Entrada desde izquierda
gsap.utils.toArray('.animate-from-left').forEach(el => {
gsap.from(el, {
x: -60,
opacity: 0,
duration: 0.7,
ease: 'power2.out',
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none'
}
});
});
// Entrada desde derecha
gsap.utils.toArray('.animate-from-right').forEach(el => {
gsap.from(el, {
x: 60,
opacity: 0,
duration: 0.7,
ease: 'power2.out',
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none'
}
});
});
// Escalonado para grupos (cards, features, timeline steps)
gsap.utils.toArray('.animate-stagger-group').forEach(group => {
const items = group.querySelectorAll('.stagger-item');
gsap.from(items, {
y: 40,
opacity: 0,
duration: 0.6,
stagger: 0.12,
ease: 'power2.out',
scrollTrigger: {
trigger: group,
start: 'top 80%',
toggleActions: 'play none none none'
}
});
});
}
Patrón 4: Split text (Texto que aparece por palabras/líneas)
Para secciones de manifiesto o titulares dramáticos.
function initSplitText(selector) {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
const text = el.textContent;
const words = text.split(' ');
el.innerHTML = words.map(word =>
`<span class="inline-block overflow-hidden"><span class="split-word inline-block">${word}</span></span>`
).join(' ');
gsap.from(el.querySelectorAll('.split-word'), {
y: '100%',
opacity: 0,
duration: 0.6,
stagger: 0.05,
ease: 'power3.out',
scrollTrigger: {
trigger: el,
start: 'top 80%',
toggleActions: 'play none none none'
}
});
});
}
Patrón 5: Typewriter (Texto que se escribe solo)
Efecto de terminal/telemetría con cursor intermitente.
function initTypewriter(selector, messages, speed = 50) {
const el = document.querySelector(selector);
let messageIndex = 0;
let charIndex = 0;
let isDeleting = false;
function type() {
const currentMessage = messages[messageIndex];
if (isDeleting) {
el.textContent = currentMessage.substring(0, charIndex - 1);
charIndex--;
} else {
el.textContent = currentMessage.substring(0, charIndex + 1);
charIndex++;
}
let delay = isDeleting ? speed / 2 : speed;
if (!isDeleting && charIndex === currentMessage.length) {
delay = 2000; // Pausa al completar
isDeleting = true;
} else if (isDeleting && charIndex === 0) {
isDeleting = false;
messageIndex = (messageIndex + 1) % messages.length;
delay = 500;
}
setTimeout(type, delay);
}
type();
}
// Uso:
// initTypewriter('.typewriter-text', [
// 'Optimizando tu embudo…',
// 'Generando landing…',
// 'Ajustando conversiones…'
// ]);
CSS para el cursor:
.typewriter-container::after {
content: '|';
animation: blink 0.8s infinite;
color: var(--color-accent);
font-weight: 300;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
Patrón 6: Acordeón (FAQ)
Acordeón con transición suave de altura.
function initAccordion() {
const items = document.querySelectorAll('.accordion-item');
items.forEach(item => {
const trigger = item.querySelector('.accordion-trigger');
const content = item.querySelector('.accordion-content');
const icon = item.querySelector('.accordion-icon');
// Iniciar cerrado
gsap.set(content, { height: 0, overflow: 'hidden' });
trigger.addEventListener('click', () => {
const isOpen = item.classList.contains('is-open');
// Cerrar todos los demás
items.forEach(other => {
if (other !== item && other.classList.contains('is-open')) {
other.classList.remove('is-open');
gsap.to(other.querySelector('.accordion-content'), {
height: 0,
duration: 0.4,
ease: 'power2.inOut'
});
gsap.to(other.querySelector('.accordion-icon'), {
rotation: 0,
duration: 0.3
});
}
});
// Toggle actual
if (isOpen) {
item.classList.remove('is-open');
gsap.to(content, { height: 0, duration: 0.4, ease: 'power2.inOut' });
gsap.to(icon, { rotation: 0, duration: 0.3 });
} else {
item.classList.add('is-open');
gsap.to(content, { height: 'auto', duration: 0.4, ease: 'power2.inOut' });
gsap.to(icon, { rotation: 45, duration: 0.3 });
}
});
});
}
Patrón 7: Parallax sutil en fondos
Movimiento sutil del fondo al hacer scroll para dar profundidad.
function initParallax() {
gsap.utils.toArray('.parallax-bg').forEach(bg => {
gsap.to(bg, {
yPercent: -20,
ease: 'none',
scrollTrigger: {
trigger: bg.parentElement,
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
});
});
}
HTML necesario:
<section class="relative overflow-hidden">
<div class="parallax-bg absolute inset-0 -top-[20%] -bottom-[20%]">
<img src="imagen.jpg" class="w-full h-full object-cover" alt="Descripción">
</div>
<div class="relative z-10">
<!-- Contenido de la sección -->
</div>
</section>
Patrón 8: Counter animado (Números que suben)
Para secciones de métricas o prueba social.
function initCounters() {
gsap.utils.toArray('.counter').forEach(counter => {
const target = parseInt(counter.getAttribute('data-target'));
const suffix = counter.getAttribute('data-suffix') || '';
gsap.from(counter, {
textContent: 0,
duration: 2,
ease: 'power1.out',
snap: { textContent: 1 },
scrollTrigger: {
trigger: counter,
start: 'top 85%',
toggleActions: 'play none none none'
},
onUpdate: function() {
counter.textContent = Math.ceil(parseFloat(counter.textContent)) + suffix;
}
});
});
}
HTML:
<span class="counter" data-target="147" data-suffix="+">0</span>
Patrón 9: Menú hamburger móvil
Toggle de menú en móvil con animación.
function initMobileMenu() {
const menuBtn = document.querySelector('.menu-toggle');
const mobileMenu = document.querySelector('.mobile-menu');
const menuLinks = mobileMenu.querySelectorAll('a');
let isOpen = false;
menuBtn.addEventListener('click', () => {
isOpen = !isOpen;
menuBtn.classList.toggle('is-active');
if (isOpen) {
gsap.to(mobileMenu, {
clipPath: 'circle(150% at top right)',
duration: 0.5,
ease: 'power3.inOut'
});
gsap.from(menuLinks, {
y: 20,
opacity: 0,
stagger: 0.08,
duration: 0.4,
delay: 0.2
});
} else {
gsap.to(mobileMenu, {
clipPath: 'circle(0% at top right)',
duration: 0.4,
ease: 'power2.in'
});
}
});
// Cerrar al hacer click en un link
menuLinks.forEach(link => {
link.addEventListener('click', () => {
isOpen = false;
menuBtn.classList.remove('is-active');
gsap.to(mobileMenu, {
clipPath: 'circle(0% at top right)',
duration: 0.4,
ease: 'power2.in'
});
});
});
}
CSS inicial del menú:
.mobile-menu {
clip-path: circle(0% at top right);
position: fixed;
inset: 0;
z-index: 999;
}
Patrón 10: Punto "en vivo" pulsante
Indicador de estado tipo "sistema activo".
.pulse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #22c55e;
position: relative;
display: inline-block;
}
.pulse-dot::before {
content: '';
position: absolute;
inset: -3px;
border-radius: 50%;
background-color: #22c55e;
animation: pulse-ring 2s ease-out infinite;
}
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(2.5); opacity: 0; }
}
HTML:
<span class="pulse-dot"></span> Sistema activo
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.