Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

Scrollytelling Web: Kỹ Thuật Scroll Storytelling Apple Dùng Cho Website Việt 2026

Ếch Trendy
Ếch Trendy

7 tháng trước · 9 phút đọc

Tại sao Apple ghim người dùng hàng phút chỉ nhờ cuộn trang?

Mở trang giới thiệu iPhone bất kỳ của Apple. Bạn cuộn xuống - điện thoại xoay, chi tiết camera zoom vào, text fade-in đúng lúc, background chuyển màu theo từng section. Không có nút nào. Không popup nào. Chỉ scroll.

Đó là scrollytelling - kỹ thuật kể chuyện qua scroll mà Apple, Linear, Stripe đang dùng để giữ người dùng. Và con số không nhỏ: from 30 seconds in 2024 to 50 seconds in 2025 (Flourish Studio Blog).

342325 Scroll xuống = câu chuyện mở ra - đây là logic cốt lõi của scrollytelling

Scrollytelling không phải trend mới. Nhưng năm 2026 nó mới thật sự dễ implement nhờ GSAP ScrollTrigger trở nên hoàn toàn miễn phí, cộng với React/Next.js ecosystem trưởng thành. Bài này mình sẽ đi từ khái niệm, phân tích kỹ thuật Apple dùng, đến code chạy được cho landing page Việt - không lý thuyết suông.


Scrollytelling là gì và tại sao 2026 là thời điểm làm?

Định nghĩa đơn giản: scrollytelling đồng bộ animation, visual, text với vị trí scroll của người dùng. Cuộn đến đâu, câu chuyện diễn ra đến đó.

Khác với animation thông thường (tự chạy theo thời gian), scroll-driven animation do người dùng kiểm soát. Họ có thể dừng, cuộn chậm, cuộn nhanh - content theo. Cảm giác interactive mà không cần click gì cả.

342326 Animation thường vs scroll-driven: người dùng kiểm soát tiến độ câu chuyện

Tại sao 2026 là thời điểm tốt để làm?

Ba lý do cụ thể:

  1. GSAP ScrollTrigger miễn phí hoàn toàn - trước đây một số tính năng cần license Club GSAP. Từ một thời điểm gần đây, toàn bộ plugin bao gồm ScrollSmoother đã free.

  2. CSS Scroll-driven animations native đã được support trên Chrome 115+, Edge 115+, Opera 101+ support CSS scroll-driven animations; Firefox and Safari do not support it as of 2024-2025 (MDN Web Docs & Can I Use) browser hiện đại - nghĩa là bạn có option không cần library với hiệu ứng cơ bản.

  3. React/Next.js pattern đã ổn định - useEffect + ScrollTrigger.refresh() sau hydration không còn là vấn đề đau đầu như 2023.

ScrollMagic - tool phổ biến trước đây - đã không còn được maintain tích cực từ 2016. GSAP ScrollTrigger là standard de facto hiện tại.


Apple làm gì với scroll? Phân tích kỹ thuật

Trang sản phẩm iPhone của Apple dùng ít nhất 3 kỹ thuật scrollytelling cốt lõi mà GSAP implement được ngay.

Kỹ thuật 1: Image sequence zoom

Apple preload hàng trăm frame ảnh sản phẩm (thường 200-300 frames), render lên <canvas>, rồi map từng frame với scroll position. Cuộn xuống = video tua nhanh. Thực ra là ảnh vẽ lên canvas theo thứ tự.

// Preload frames trong useEffect
const frames = [];
const totalFrames = 280;

useEffect(() => {
  for (let i = 1; i <= totalFrames; i++) {
    const img = new Image();
    // Đặt tên file nhất quán: frame-001.jpg, frame-002.jpg...
    img.src = `/frames/frame-${String(i).padStart(3, '0')}.jpg`;
    frames.push(img);
  }

  // Kết nối scroll với frame index
  gsap.to({ frame: 0 }, {
    frame: totalFrames - 1,
    snap: 'frame',
    scrollTrigger: {
      trigger: canvasRef.current,
      start: 'top top',
      end: '+=3000', // Scroll 3000px = xem hết animation
      scrub: 0.5,
      pin: true // Giữ canvas cố định khi scroll
    },
    onUpdate() {
      // Vẽ frame tương ứng lên canvas
      ctx.drawImage(frames[Math.round(this.targets()[0].frame)], 0, 0);
    }
  });
}, []);

342327 280 frames + canvas render = hiệu ứng video zoom mượt như Apple

Kỹ thuật 2: Pinning - ghim section trong viewport

Khi bạn scroll qua section giới thiệu camera trên iPhone page, background đứng yên trong khi nội dung bên trong reveal dần. Đó là pin: true của ScrollTrigger.

gsap.timeline({
  scrollTrigger: {
    trigger: '.product-section',
    start: 'top top',
    end: 'bottom bottom',
    scrub: 1,
    pin: true // Ghim section trong viewport
  }
})
.from('.feature-text', { opacity: 0, y: 50 })
.from('.feature-image', { scale: 0.8, opacity: 0 }, '<0.2');

Kỹ thuật 3: Card stack với scale/rotation

AWWWARDS-style stack - cards chồng lên nhau, mỗi card scale và xoay nhẹ khi scroll qua. Rất phù hợp với section "Tính năng nổi bật" của landing page.

const cards = gsap.utils.toArray('.stack-card');

cards.forEach((card, i) => {
  gsap.to(card, {
    scale: 1 - (cards.length - i) * 0.05, // Card dưới nhỏ hơn
    rotation: i % 2 === 0 ? -3 : 3, // Xoay xen kẽ
    scrollTrigger: {
      trigger: card,
      start: 'top center',
      end: 'bottom top',
      scrub: 1
    }
  });
});

Setup GSAP ScrollTrigger trong Next.js App Router

Next.js App Router + GSAP có một số gotcha nếu không biết. Mình đã mất cả ngày debug vì quên invalidateOnRefresh.

Cài đặt:

npm install gsap

GSAP và ScrollTrigger hiện tại không cần license. Install là dùng được hết.

Component wrapper chuẩn cho Next.js:

'use client'; // BẮT BUỘC - GSAP chạy client-side

import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrollSmoother } from 'gsap/ScrollSmoother';

// Đăng ký plugin một lần duy nhất
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);

export default function ScrollySection() {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // Khởi tạo ScrollSmoother cho smooth scroll
    const smoother = ScrollSmoother.create({
      smooth: 1.5,       // Độ trễ scroll (cao = mượt hơn)
      normalizeScroll: true, // Fix cross-browser
      ignoreMobileResize: true
    });

    const ctx = gsap.context(() => {
      // Animation của bạn ở đây
      gsap.from('.hero-title', {
        opacity: 0,
        y: 100,
        scrollTrigger: {
          trigger: '.hero-title',
          start: 'top 80%',
          end: 'top 30%',
          scrub: 1,
          invalidateOnRefresh: true // Quan trọng cho SSR
        }
      });
    }, containerRef);

    // Refresh sau hydration để tính lại vị trí
    ScrollTrigger.refresh();

    // Cleanup tránh memory leak
    return () => {
      ctx.revert();
      smoother.kill();
    };
  }, []);

  return (
    <div id="smooth-wrapper">
      <div id="smooth-content" ref={containerRef}>
        {/* Nội dung */}
      </div>
    </div>
  );
}

342328 invalidateOnRefresh: true là dòng code tránh bug scroll position sai sau SSR

3 điều quan trọng với Next.js:

  • 'use client' directive bắt buộc - GSAP không chạy được trên server
  • gsap.context() + ctx.revert() để cleanup khi unmount, tránh leak
  • ScrollTrigger.refresh() sau useEffect để recalculate positions sau hydration

Nếu bạn đang học React từ đầu, khóa Xây Dựng Website với ReactJS cover useEffect và lifecycle rất kỹ - đây là nền tảng cần có trước khi đụng vào GSAP.


Ứng dụng thực tế: Landing page Việt với scrollytelling

Lý thuyết đủ rồi. Mình sẽ demo một pattern thực tế hay gặp ở landing page cho startup Việt: "Tính năng nổi bật" section với pinned text + image reveal.

Pattern này phù hợp với: app fintech, e-commerce, SaaS B2B - bất kỳ page nào cần giới thiệu nhiều tính năng mà không muốn làm người dùng scroll mệt.

// FeatureScrolly.tsx - Component sẵn dùng
'use client';

import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

const features = [
  { title: 'Thanh toán tức thì', desc: 'Giao dịch dưới 2 giây', img: '/feature-1.png' },
  { title: 'Bảo mật 256-bit', desc: 'Mã hóa bank-grade', img: '/feature-2.png' },
  { title: 'Dashboard realtime', desc: 'Xem báo cáo theo giây', img: '/feature-3.png' },
];

export default function FeatureScrolly() {
  const sectionRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const ctx = gsap.context(() => {
      const panels = gsap.utils.toArray<HTMLElement>('.feature-panel');

      // Ghim section trong khi scroll qua từng feature
      ScrollTrigger.create({
        trigger: sectionRef.current,
        start: 'top top',
        end: `+=${panels.length * 100}vh`,
        pin: true,
        scrub: 1,
      });

      // Mỗi panel fade-in khi scroll đến
      panels.forEach((panel, i) => {
        gsap.from(panel, {
          opacity: 0,
          x: 80,
          scrollTrigger: {
            trigger: panel,
            start: `${i * 33}% top`, // Chia đều scroll range
            end: `${(i + 1) * 33}% top`,
            scrub: 1,
          }
        });
      });
    }, sectionRef);

    ScrollTrigger.refresh();
    return () => ctx.revert();
  }, []);

  return (
    <section ref={sectionRef} className="relative h-screen overflow-hidden">
      <div className="sticky top-0 h-screen flex items-center">
        {features.map((f, i) => (
          <div key={i} className="feature-panel absolute inset-0 flex gap-16 p-20">
            <div className="flex-1">
              <h3 className="text-4xl font-bold">{f.title}</h3>
              <p className="text-xl mt-4 text-gray-600">{f.desc}</p>
            </div>
            <img src={f.img} className="flex-1 object-contain" />
          </div>
        ))}
      </div>
    </section>
  );
}

342329 Pinned section + feature reveal từng tính năng theo scroll - phù hợp mọi landing page Việt

Pattern này giải quyết một vấn đề thực tế: landing page Việt thường nhét quá nhiều thông tin vào một trang, người dùng scroll qua mà không đọc. Khi pin section lại và reveal từng feature, người dùng buộc phải "ở lại" với mỗi tính năng ít nhất vài giây.

Mình có demo HTML chạy được để bạn thử ngay:

Tải demo scrollytelling landing page


Performance và những thứ cần cẩn thận

Scrollytelling đẹp nhưng nếu làm sai, FPS drop xuống 20 và người dùng sẽ thoát nhanh hơn là ở lại.

Vấn đề phổ biến nhất: layout thrashing

Khi GSAP animate top, left, width, height - browser phải reflow layout mỗi frame. Với 60fps, đó là 60 lần reflow/giây. Trang chết.

Luôn animate bằng transform và opacity thay vì positional properties:

// ❌ Gây layout thrashing
gsap.to('.box', { top: '100px', left: '50px' });

// ✅ Chỉ dùng GPU - không reflow
gsap.to('.box', { y: 100, x: 50, opacity: 0.5 });

Image sequence: lazy load theo batch

Nếu preload 280 frames cùng lúc, page sẽ block render. Load theo batch 20-30 frames:

// Preload theo batch, không load hết cùng lúc
async function preloadFramesBatch(start: number, end: number) {
  const promises = [];
  for (let i = start; i <= end; i++) {
    promises.push(new Promise<HTMLImageElement>((resolve) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.src = `/frames/frame-${String(i).padStart(3, '0')}.jpg`;
    }));
  }
  return Promise.all(promises);
}

342330 transform vs positional properties: chênh lệch GPU workload rõ ràng

Mobile: giảm hoặc disable

ScrollSmoother trên mobile có thể làm trang cảm giác "nặng" và gây motion sickness. Pattern mình hay dùng:

const isMobile = window.innerWidth < 768;

ScrollSmoother.create({
  smooth: isMobile ? 0 : 1.5, // Tắt smooth trên mobile
  normalizeScroll: true,
});

Checklist trước khi ship:

  • [ ] Test Lighthouse Performance score > 75 sau khi thêm scroll animation
  • [ ] Kiểm tra prefers-reduced-motion để disable animation cho người dùng nhạy cảm
  • [ ] will-change: transform trên elements được animate thường xuyên
  • [ ] Cleanup tất cả ScrollTrigger instances trong useEffect return
// Tôn trọng người dùng cần giảm chuyển động
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; }
}

Bắt đầu từ đâu nếu bạn mới làm quen?

Scrollytelling không cần học từ đầu đến cuối mới làm được. Mình đề xuất thứ tự này:

Tuần 1: Làm quen GSAP cơ bản - gsap.to(), gsap.from(), gsap.timeline(). Không cần ScrollTrigger, chỉ cần animate vài element.

Tuần 2: Thêm ScrollTrigger với triggerstart/end đơn giản. Bắt đầu bằng fade-in elements khi scroll đến - pattern phổ biến nhất và dễ nhất.

Tuần 3: Thử scrub: 1pin: true. Đây là bước nhảy lớn nhất về complexity nhưng cũng là nơi magic xảy ra.

Tuần 4: Image sequence hoặc WebGL nếu project cần. Đây là advanced - chỉ làm khi cần thiết.

342331 4 tuần từ zero đến scroll animation cho landing page thực tế

Nền tảng JavaScript vững giúp bạn debug nhanh hơn rất nhiều khi GSAP không hoạt động như mong đợi. Nếu cảm thấy còn lung túng với JS, khóa JavaScript Pro có phần closure và execution context giúp hiểu tại sao GSAP cần cleanup trong useEffect.

Một thực tế mình quan sát: Hầu hết landing page Việt chưa dùng bất kỳ scroll animation nào ngoài fade-in CSS đơn giản. Đây vừa là hạn chế, vừa là cơ hội - implement scrollytelling đúng cách có thể tạo khác biệt rõ so với đối thủ.

Bắt đầu nhỏ: thêm ScrollTrigger cho một section duy nhất trong dự án hiện tại. Nếu thấy hoạt động tốt, mở rộng dần. Đừng refactor toàn bộ trang từ đầu.

Code từ bài này chạy được ngay - clone về, thay nội dung, đổi màu theo brand, ship thôi. Gặp vướng mắc ở bước nào, drop comment bên dưới nhé.

Tài liệu tham khảo thêm: GSAP ScrollTrigger - Getting Started