→ לכל המאגר

B49 · טאבים שמתקדמים לבד עם פס זמן

התנהגויות GSAP · ScrollTrigger כרטיסיםתהליך ושלביםתמונות וגלריות ממתין

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

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

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

הדמו

מתחילים בהבנה

שיחה אחת שממפה את העסק, את הלקוח ואת מה שצריך לקרות באתר.

  • מיפוי קהל ומסרים
  • מבנה עמודים
1

כותבים לפני שמעצבים

הטקסט קובע את המבנה, ולא להפך. כל סקשן עונה על השאלה הבאה בתור.

  • כותרות שמדברות ללקוח
  • קריאה לפעולה אחת
2

שפה אחת בכל עמוד

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

  • מערכת עיצוב
  • התאמה לכל מסך
3

יודעים מה עובד

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

  • מעקב המרות
  • דוח חודשי
4
פס הזמן אינו קישוט אלא הטיימר עצמו: ה-onComplete שלו הוא מה שמעביר טאב, ולכן אין שני מנגנוני זמן שיכולים להתפצל. `width` נבחר על פני `scaleX` כדי שהפס יישאר חד בקצוות, והוא מעוגן ל-right כי בעברית הוא צריך לגדול מימין לשמאל. הסקשן מתחיל לרוץ רק כשהוא נראה, נעצר בהובר ובפוקוס, ובמצב חיסכון בתנועה הופך לטאבים רגילים לחלוטין.

קוד להדבקה

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

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

CSS

/* מחלקות עזר של המאגר שהרכיב הזה משתמש בהן */
.stage{padding:var(--sec, clamp(120px, 12vw, 260px)) var(--gutter, clamp(24px, 5vw, 96px));position:relative}
.stage.tight{padding-block:clamp(48px,5vw,90px)}
.stage.full{padding-inline:0}
.ph{display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;border-radius:var(--r, 14px)}
.ph-a{background:linear-gradient(160deg,#3b5bdb,#748ffc)}
.ph-c{background:linear-gradient(160deg,#0b7285,#3bc9db)}
.ph-d{background:linear-gradient(160deg,#2b8a3e,#69db7c)}
.ph-e{background:linear-gradient(160deg,#e8590c,#ffa94d)}
.ph-l{background:rgba(16,18,43,.72);color:#fff;padding:.18em .7em;border-radius:999px;line-height:1.3;font-size:max(.78em,12px)}

/* B49 · טאבים שמתקדמים לבד עם פס זמן */
.at{max-width:min(1020px,94vw);margin-inline:auto}
.at-links{display:flex;gap:8px;flex-wrap:wrap;border-bottom:1px solid var(--line, #e4e4ee);margin-bottom:clamp(20px,3vw,34px)}
.at-btn{position:relative;border:0;background:none;font:inherit;font-size:16px;color:var(--muted, #6a6d85);
  padding:12px 4px 16px;margin-inline-end:20px;cursor:pointer;transition:color .25s}
.at-btn.on{color:var(--ink, #16182b);font-weight:600}
.at-bar{position:absolute;bottom:-1px;right:0;height:2px;width:0;background:var(--accent, #4a3aff);border-radius:2px}
.at-panel{display:none;grid-template-columns:1fr 1fr;gap:clamp(20px,3vw,50px);align-items:center}
.at-panel.on{display:grid}
.at-panel h4{margin:0 0 10px;font-size:clamp(20px,2.4vw,32px)}
.at-panel p{margin:0;color:var(--muted, #6a6d85);font-size:17px;line-height:1.75;max-width:44ch}
.at-panel ul{margin:16px 0 0;padding:0;list-style:none;display:grid;gap:8px;color:var(--muted, #6a6d85);font-size:15px}
.at-panel li::before{content:"✓";color:#2b8a3e;font-weight:700;margin-inline-end:8px}
.at-media{aspect-ratio:4/3;font-size:30px}
@media(max-width:760px){.at-panel.on{grid-template-columns:1fr}.at-links{overflow-x:auto;flex-wrap:nowrap}}

HTML

<div class="stage tight"><div class="at">
  <div class="at-links">
    <button class="at-btn on">אפיון<i class="at-bar"></i></button>
    <button class="at-btn">קופי<i class="at-bar"></i></button>
    <button class="at-btn">עיצוב<i class="at-bar"></i></button>
    <button class="at-btn">מדידה<i class="at-bar"></i></button>
  </div>
  <div class="at-panel on"><div><h4>מתחילים בהבנה</h4><p>שיחה אחת שממפה את העסק, את הלקוח ואת מה שצריך לקרות באתר.</p>
    <ul><li>מיפוי קהל ומסרים</li><li>מבנה עמודים</li></ul></div><div class="ph at-media ph-a"><span class="ph-l">1</span></div></div>
  <div class="at-panel"><div><h4>כותבים לפני שמעצבים</h4><p>הטקסט קובע את המבנה, ולא להפך. כל סקשן עונה על השאלה הבאה בתור.</p>
    <ul><li>כותרות שמדברות ללקוח</li><li>קריאה לפעולה אחת</li></ul></div><div class="ph at-media ph-c"><span class="ph-l">2</span></div></div>
  <div class="at-panel"><div><h4>שפה אחת בכל עמוד</h4><p>מערכת צבעים, טיפוגרפיה וריווח שחוזרת בכל מקום, כך שהאתר מרגיש שלם.</p>
    <ul><li>מערכת עיצוב</li><li>התאמה לכל מסך</li></ul></div><div class="ph at-media ph-d"><span class="ph-l">3</span></div></div>
  <div class="at-panel"><div><h4>יודעים מה עובד</h4><p>מחברים מעקב לפני העלייה לאוויר, ומשפרים לפי נתונים במקום לפי תחושה.</p>
    <ul><li>מעקב המרות</li><li>דוח חודשי</li></ul></div><div class="ph at-media ph-e"><span class="ph-l">4</span></div></div>
</div></div>

JS

gsap.registerPlugin(ScrollTrigger);

(function(){
  const DUR=5;
  const btns=[...document.querySelectorAll(".at-btn")],panels=[...document.querySelectorAll(".at-panel")];
  const reduce=matchMedia("(prefers-reduced-motion: reduce)").matches;
  let i=0,timer,running=false;
  function show(n){
    i=n;
    btns.forEach((b,k)=>b.classList.toggle("on",k===n));
    panels.forEach((p,k)=>p.classList.toggle("on",k===n));
    if(!reduce)gsap.fromTo(panels[n],{opacity:0,y:18},{opacity:1,y:0,duration:.5,ease:"power2.out"});
    if(timer)timer.kill();
    gsap.set(".at-bar",{width:0});
    if(reduce||!running)return;
    // הפס הוא השעון: כשהוא מגיע לסוף עוברים לטאב הבא
    timer=gsap.to(btns[n].querySelector(".at-bar"),{width:"100%",duration:DUR,ease:"none",
      onComplete:()=>show((n+1)%btns.length)});
  }
  btns.forEach((b,k)=>b.addEventListener("click",()=>show(k)));
  const at=document.querySelector(".at");
  at.addEventListener("mouseenter",()=>timer&&timer.pause());
  at.addEventListener("mouseleave",()=>timer&&timer.resume());
  at.addEventListener("focusin",()=>timer&&timer.pause());
  // מתחילים רק כשהסקשן על המסך, אחרת הטאבים "בוזבזו" לפני שהמבקר הגיע
  ScrollTrigger.create({trigger:at,start:"top 75%",end:"bottom 25%",
    onToggle:self=>{running=self.isActive;if(self.isActive)show(i);else if(timer)timer.pause();}});
  show(0);
})();