bodev.vn

Tiến độ dự án cuộn ngang

Cuộn dọc nhưng nội dung chạy ngang. Đường tiến độ vẽ dần theo từng mốc.

Cách hoạt động

  1. 1.ScrollTrigger ghim section lại, rồi đổi cuộn dọc thành chạy ngang.
  2. 2.Mỗi mốc sáng lên khi chạy tới giữa màn hình, nhờ containerAnimation.
  3. 3.Trong khung demo thì cuộn bên trong khung. Trên trang thật thì chạy theo cuộn của trang.

Thử chỉnh

Thêm, bớt mốc
Copy hoặc xoá một khối .milestone trong HTML. JS tự tính lại.
Cuộn lâu hơn mới hết
Tăng hệ số 1.2 trong end của ScrollTrigger.
Tắt hiệu ứng trên điện thoại
Bọc đoạn tạo ScrollTrigger trong gsap.matchMedia("(min-width: 768px)").

Code tham khảo

<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tiến độ dự án cuộn ngang · bodev.vn</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;700;800&display=swap" rel="stylesheet" />
<style>
:root {
  --bg: #12151a;
  --text: #f2efe9;
  --muted: #8d939c;
  --accent: #e2b04a;
  --card: #1b2028;
  --line: #2a303a;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: "Be Vietnam Pro", system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
  overflow-x: hidden;
}

.intro,
.outro {
  min-height: 70vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 40px clamp(20px, 6vw, 80px);
}
.kicker { margin: 0; color: var(--accent); font-size: 13px; letter-spacing: 0.12em; text-transform: uppercase; }
.intro h1 { margin: 8px 0 0; font-size: clamp(40px, 7vw, 88px); font-weight: 800; line-height: 1; }
.lead { margin: 16px 0 0; color: var(--muted); }

.timeline {
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
}
.timeline-head { padding: 0 clamp(20px, 6vw, 80px) 32px; }
.timeline-head h2 { margin: 0; font-size: clamp(24px, 3vw, 36px); }
.timeline-head p { margin: 6px 0 0; color: var(--accent); }

.track {
  position: relative;
  display: flex;
  gap: 40px;
  width: max-content;
  padding: 0 clamp(20px, 6vw, 80px);
}
.line {
  position: absolute;
  left: 0;
  right: 0;
  top: 11px;
  height: 3px;
  background: var(--line);
}
.line-fill {
  height: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
}

.milestone { position: relative; width: min(340px, 78vw); padding-top: 40px; }
.milestone::before {
  content: "";
  position: absolute;
  top: 4px;
  left: 0;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--bg);
  border: 3px solid var(--line);
}
.milestone.done::before { background: var(--accent); border-color: var(--accent); }
.milestone.current::before { border-color: var(--accent); box-shadow: 0 0 0 6px rgba(226, 176, 74, 0.2); }
.milestone time { display: block; margin-bottom: 12px; font-weight: 700; font-size: 20px; }

.card { padding: 24px; border-radius: 18px; background: var(--card); }
.card h3 { margin: 12px 0 8px; font-size: 22px; }
.card p { margin: 0; color: var(--muted); line-height: 1.6; }
.badge {
  display: inline-block;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  background: var(--line);
}
.done .badge { background: rgba(226, 176, 74, 0.15); color: var(--accent); }
.current .badge { background: var(--accent); color: var(--bg); }

.outro { align-items: flex-start; }
.outro h2 { margin: 0 0 24px; font-size: clamp(28px, 4vw, 48px); }
.btn {
  /* CTA trong demo là <button>, không gắn link. Nối vào form hoặc popup của bạn. */
  border: 0;
  font: inherit;
  cursor: pointer;
  display: inline-block;
  padding: 14px 26px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--bg);
  font-weight: 700;
  text-decoration: none;
}
</style>
</head>
<body>
<section class="intro">
  <p class="kicker">An Phú Riverside</p>
  <h1>Tiến độ xây dựng</h1>
  <p class="lead">Cuộn xuống để xem từng mốc.</p>
</section>

<section class="timeline">
  <div class="timeline-head">
    <h2>Cập nhật tháng 09/2026</h2>
    <p>Đúng tiến độ cam kết</p>
  </div>
  <div class="track">
    <div class="line"><div class="line-fill"></div></div>

    <article class="milestone done">
      <time>03/2025</time>
      <div class="card">
        <span class="badge">Xong</span>
        <h3>Khởi công</h3>
        <p>Dọn mặt bằng, ép cọc thử. Đủ giấy phép xây dựng.</p>
      </div>
    </article>
    <article class="milestone done">
      <time>10/2025</time>
      <div class="card">
        <span class="badge">Xong</span>
        <h3>Xong móng, tầng hầm</h3>
        <p>2 tầng hầm, 1.100 chỗ đậu xe máy và ô tô.</p>
      </div>
    </article>
    <article class="milestone current">
      <time>09/2026</time>
      <div class="card">
        <span class="badge">Đang làm</span>
        <h3>Cất nóc tòa A</h3>
        <p>Tòa A lên tầng 20/20. Tòa B tầng 14, tòa C tầng 9.</p>
      </div>
    </article>
    <article class="milestone">
      <time>06/2027</time>
      <div class="card">
        <span class="badge">Sắp tới</span>
        <h3>Hoàn thiện mặt ngoài</h3>
        <p>Sơn, kính, cảnh quan ven sông.</p>
      </div>
    </article>
    <article class="milestone">
      <time>Q4/2027</time>
      <div class="card">
        <span class="badge">Sắp tới</span>
        <h3>Bàn giao</h3>
        <p>Bàn giao hoàn thiện cơ bản, cấp sổ hồng theo đợt.</p>
      </div>
    </article>
  </div>
</section>

<section class="outro">
  <h2>Muốn xem công trường tận nơi?</h2>
  <button type="button" class="btn">Đăng ký tham quan</button>
</section>

<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script>
// Tiến độ dự án cuộn ngang — bodev.vn/lab/tien-do-cuon-ngang
// Cần: gsap + ScrollTrigger.

gsap.registerPlugin(ScrollTrigger);

const track = document.querySelector(".track");
// Quãng đường phải chạy ngang = độ dài track trừ bề ngang màn hình.
const distance = () => track.scrollWidth - window.innerWidth;
// Hệ số > 1: cuộn nhiều hơn một chút mới hết, đọc thong thả hơn.
const SCROLL_FACTOR = 1.2;

const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;

if (reduceMotion) {
  // Không ghim, không chạy ngang: cho track cuộn ngang bằng tay.
  document.querySelector(".timeline").style.overflowX = "auto";
  gsap.set(".line-fill", { scaleX: 1 });
} else {
  // 1. Ghim section lại, kéo track sang trái theo độ cuộn.
  const scroll = gsap.to(track, {
    x: () => -distance(),
    ease: "none",
    scrollTrigger: {
      trigger: ".timeline",
      start: "top top",
      end: () => "+=" + distance() * SCROLL_FACTOR,
      pin: true,
      scrub: 1,
      invalidateOnRefresh: true,
    },
  });

  // 2. Đường tiến độ vẽ dần cùng nhịp.
  gsap.to(".line-fill", {
    scaleX: 1,
    ease: "none",
    scrollTrigger: {
      trigger: ".timeline",
      start: "top top",
      end: () => "+=" + distance() * SCROLL_FACTOR,
      scrub: 1,
      invalidateOnRefresh: true,
    },
  });

  // 3. Mỗi mốc sáng lên khi chạy tới giữa màn hình.
  // containerAnimation: đo vị trí theo chuyển động ngang, không theo cuộn dọc.
  document.querySelectorAll(".milestone").forEach((m) => {
    gsap.from(m.querySelector(".card"), {
      opacity: 0.25,
      y: 40,
      scale: 0.92,
      ease: "power2.out",
      scrollTrigger: {
        trigger: m,
        containerAnimation: scroll,
        start: "left 80%",
        end: "left 45%",
        scrub: true,
      },
    });
  });
}

// Tiêu đề đầu trang.
gsap.from(".intro > *", { opacity: 0, y: 30, duration: 0.8, stagger: 0.1, ease: "expo.out" });
</script>
</body>
</html>

Code để học cách làm, không phải thư viện. Không đảm bảo chạy ngay trên site khác, dùng thì tự kiểm tra.

Tiếp trong Bất động sảnBản đồ tiện ích quanh dự án