الحكي · فصل ٥

Horizontal Scroll و Scrollytelling — الـ scroll بيحكي

sticky + view-timeline · horizontal track · scrollytelling · sticky scene · step observer
🎯 الهدف: تحوّل الـ scroll الرأسي لحركة بالعرض، وتعمل قصة المشهد فيها ثابت والخطوات بتغيّره.

النمطين دول بيجمعوا اللي فات: sticky عشان المشهد يثبت، ومعاه driven أو triggered عشان يتحرك.

Horizontal Scroll: انزل لتحت، يمشي بالعرض

الـ scroll الرأسي بيحرّك الكروت بالعرض

اعمل scroll لتحت… والكروت تمشي بالعرض

1
2
3
4

خلصنا ✓

متصفحك لسه مش بيدعم scroll-driven animations، فالديمو ثابت. جرّبه على Chrome أو Edge أو Safari حديث.

التركيبة ٣ طبقات:

.wrap      ← طويل (مثلًا 400vh): ده اللي بيدّي مسافة scroll└── .stage ← sticky top:0 وارتفاعه قد الشاشة: بيثبت طول ما الـ wrap معدّي    └── .track ← صف كروت عرضه أكبر من الشاشة: بيتحرك translateX
horizontal.css
.wrap { height: 400vh; view-timeline: --h block; }.stage {  position: sticky;  top: 0;  height: 100vh;  overflow: hidden;  direction: ltr; /* في صفحة RTL الـ track بيتصف من اليمين، فلازم الـ stage نفسه LTR */}.track {  display: flex;  width: 400%;  animation: slide linear both;  animation-timeline: --h;  animation-range: contain 0% contain 100%;}@keyframes slide { to { transform: translateX(-75%); } }
  • view-timeline: --h بيدّي اسم للـ timeline بتاع الـ .wrap، والـ .track (ابنه) بيستخدمه.
  • contain = الفترة اللي الـ wrap مالي الشاشة فيها، وهي نفس الفترة اللي الـ stage ثابت فيها. فالحركة بتبدأ أول ما يثبت وبتخلص قبل ما يفك.
  • direction: ltr على الـ stage مش الـ track: في صفحة عربي، العنصر الأعرض من أبوه بيتصف من اليمين ويطلع لشمال، فالـ track هيبدأ وإنت شايف آخر كارت. وقعنا في دي في الديمو.
  • -75% لأن عندنا ٤ كروت: نتحرك ٣ كروت من ٤ عشان آخر كارت يبقى قدامك.

Scrollytelling: المشهد ثابت والخطوات بتحكي

المشهد ثابت فوق، وكل خطوة بتغيّره
الخطوة ١

١. أول عمود هو اللي بيكبر

٢. دلوقتي التاني

٣. والتالت في الآخر

بتشوفه في المقالات التفاعلية: رسمة أو خريطة ثابتة، والكلام بيعدّي جنبها أو تحتها، وكل فقرة بتغيّر المشهد.

story.ts
const scene = document.querySelector<HTMLElement>(".scene")!;const io = new IntersectionObserver(  (entries) => {    for (const e of entries) {      if (e.isIntersecting) scene.dataset.step = (e.target as HTMLElement).dataset.step;    }  },  // خط وهمي في نص الشاشة: الخطوة اللي تعدّيه تبقى الحالية  { rootMargin: "-50% 0px -50% 0px" },);document.querySelectorAll(".step").forEach((s) => io.observe(s));
story.css
.scene { position: sticky; top: 0; height: 60vh; }.step { min-height: 80vh; } /* مسافة كفاية بين الخطوات */.scene[data-step="2"] .bar-2 { height: 100%; background: var(--accent); }
مين بيغيّر مين
  1. الفقرة ٢ ← IntersectionObserver: عدّى الخط
  2. IntersectionObserver ← المشهد الثابت: غيّر الخطوة

خلّصت الفصل لما

🧠 اختبر نفسك
١) ليه الـ stage لازم يبقى sticky؟
عشان يفضل ثابت قدامك طول ما الـ wrap الطويل بيعدّي، والكروت هي اللي تتحرك.
٢) rootMargin -50% من فوق وتحت بيعمل إيه؟
بيخلّي الـ root خط رفيع في نص الشاشة، فالخطوة اللي تعدّي النص بس هي اللي تتحسب.