→ לכל המאגר

G60 · מילה שמתחלפת בתוך משפט בגלילה

GSAP GSAP · ScrollTrigger scrub טקסט וכותרותהירו ופתיחהתהליך ושלבים ממתין

משפט אחד שנשאר על המסך, ומילה אחת בתוכו מתגלגלת ומתחלפת בקצב הגלילה. מתחתיו מתחלף גם משפט המשנה, כך שכל מצב עומד בפני עצמו.

מתי משתמשים: הירו של עמוד הבית, סקשן שירותים, הצהרת מיצוב. במקום לכתוב ארבעה משפטים נפרדים, כותבים אחד שמתגלגל בין ארבע התשובות.

הדמו כאן עיצובי-ניטרלי בכוונה. כשהמהלך נכנס לפרויקט, מיובאת רק ההתנהגות: הצבעים, הרדיוסים, הפונטים והצללים יורשים את העיצוב של אותו פרויקט.

הדמו

אנחנו בונים אתרי תדמיתדפי נחיתהחנויותמערכות

אתר שמסביר מה אתם עושים ולמי, ומוביל לפנייה אחת ברורה.

עמוד יחיד ממוקד לקמפיין, עם מסר אחד וקריאה אחת לפעולה.

חנות שמוכרת גם בלי איש מכירות, עם מסלול קנייה קצר.

ממשק לניהול לקוחות, משימות ודוחות במקום גיליונות.

החלון הוא `overflow:clip` בגובה שורה אחת, והעמודה שבתוכו נעה בדיוק בגובה שורה בכל מעבר. הרוחב לא מוגדר בכלל: קונטיינר עמודה מקבל את רוחב הילד הרחב ביותר, ולכן המשפט מתרחב ומתכווץ מעצמו סביב המילה. **ה-`snap` הוא מה שהופך את זה לשמיש**: בלעדיו אפשר לעצור באמצע גלגול ולראות חצי מילה למעלה וחצי למטה. המדידה של גובה השורה נעשית בתוך פונקציה (`y:()=>...`) ולא כמספר קבוע, כך שהיא מחושבת מחדש בכל refresh וגם אחרי טעינת הפונט.

קוד להדבקה

הקוד כאן עומד בפני עצמו. כל טוקן עיצוב נושא ברירת מחדל, מחלקות העזר של המאגר מוטמעות בו, ותגי הסקריפט מסודרים לפי הסדר הנכון. פרויקט שמגדיר --accent או --line משלו יורש אותם אוטומטית. הרכיב הזה מסתמך על dir="rtl".

פתח קובץ עצמאי ↗
הצג את הקוד כאן

CSS

/* G60 · מילה שמתחלפת בתוך משפט בגלילה */
.ws{height:300vh;position:relative}
.ws-stick{position:sticky;top:0;height:100vh;display:grid;place-items:center;padding-inline:var(--gutter, clamp(24px, 5vw, 96px))}
.ws-line{display:flex;align-items:baseline;gap:.28em;flex-wrap:wrap;justify-content:center;
  font-size:clamp(28px,5.4vw,74px);font-weight:800;line-height:1.16;text-align:center}
/* החלון בגובה שורה אחת; רוחבו נקבע מאליו לפי המילה הרחבה ביותר */
.ws-win{display:inline-block;overflow:clip;height:1.16em;vertical-align:baseline}
.ws-stack{display:flex;flex-direction:column;will-change:transform}
.ws-stack span{height:1.16em;line-height:1.16em;color:var(--accent, #4a3aff);white-space:nowrap}
.ws-subs{position:relative;margin-top:clamp(18px,2.4vw,32px);height:3.4em;width:min(46ch,90vw)}
.ws-subs p{position:absolute;inset:0;margin:0;text-align:center;color:var(--muted, #6a6d85);
  font-size:clamp(15px,1.6vw,20px);line-height:1.7}
.ws-dots{position:absolute;bottom:9vh;inset-inline:0;display:flex;gap:8px;justify-content:center}
.ws-dots i{width:7px;height:7px;border-radius:50%;background:var(--line, #e4e4ee)}
.ws-dots i.on{background:var(--accent, #4a3aff)}

HTML

<div class="ws"><div class="ws-stick"><div>
  <div class="ws-line">
    <span>אנחנו בונים</span>
    <span class="ws-win"><span class="ws-stack">
      <span>אתרי תדמית</span><span>דפי נחיתה</span><span>חנויות</span><span>מערכות</span>
    </span></span>
  </div>
  <div class="ws-subs">
    <p>אתר שמסביר מה אתם עושים ולמי, ומוביל לפנייה אחת ברורה.</p>
    <p>עמוד יחיד ממוקד לקמפיין, עם מסר אחד וקריאה אחת לפעולה.</p>
    <p>חנות שמוכרת גם בלי איש מכירות, עם מסלול קנייה קצר.</p>
    <p>ממשק לניהול לקוחות, משימות ודוחות במקום גיליונות.</p>
  </div>
  <div class="ws-dots"><i class="on"></i><i></i><i></i><i></i></div>
</div></div></div>

JS

gsap.registerPlugin(ScrollTrigger);

(function(){
  const stack=document.querySelector(".ws-stack");
  const words=gsap.utils.toArray(".ws-stack span");
  const subs=gsap.utils.toArray(".ws-subs p");
  const dots=gsap.utils.toArray(".ws-dots i");
  const n=words.length;
  const reduce=matchMedia("(prefers-reduced-motion: reduce)").matches;
  gsap.set(subs,{opacity:0});gsap.set(subs[0],{opacity:1});
  if(reduce)return;
  const tl=gsap.timeline({scrollTrigger:{trigger:".ws",start:"top top",end:"bottom bottom",scrub:.5,
    // snap לנקודות שבהן מילה יושבת בול בחלון, אחרת נעצרים על חצי מילה
    snap:{snapTo:gsap.utils.snap(1/(n-1)),duration:.25,delay:.05,ease:"power2.inOut"}}});
  for(let i=1;i<n;i++){
    // כל מעבר מזיז את העמודה בדיוק בגובה שורה אחת. המדידה בפונקציה, כדי שתתעדכן ב-refresh
    tl.to(stack,{y:()=>-i*words[0].offsetHeight,ease:"power2.inOut",duration:1},i-1)
      .to(subs[i-1],{opacity:0,duration:.35},(i-1)+.1)
      .to(subs[i],{opacity:1,duration:.45},(i-1)+.4);
  }
  // הנקודות נגזרות מהמצב בפועל ולא מטוויין נפרד, ולכן הן תמיד מסונכרנות
  ScrollTrigger.create({trigger:".ws",start:"top top",end:"bottom bottom",
    onUpdate:self=>{const i=Math.round(self.progress*(n-1));
      dots.forEach((d,k)=>d.classList.toggle("on",k===i));}});
})();