→ לכל המאגר

B20 · מחיר שמתגלגל בהחלפת מסלול

התנהגויות GSAP · timeline מספרים ונתוניםטעינה ופידבק ממתין

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

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

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

הדמו

לחודש, ללא התחייבות

המחיר עטוף ב-aria-live="polite" כדי שקורא מסך יקריא את הערך החדש. הספרות ב-direction:ltr כי מספרים נכתבים משמאל לימין גם בעמוד עברי.

קוד להדבקה

הקוד כאן עומד בפני עצמו. כל טוקן עיצוב נושא ברירת מחדל, מחלקות העזר של המאגר מוטמעות בו, ותגי הסקריפט מסודרים לפי הסדר הנכון. פרויקט שמגדיר --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}

/* B20 · מחיר שמתגלגל בהחלפת מסלול */
.pr-card{max-width:420px;margin-inline:auto;background:var(--card, #fff);border:1px solid var(--line, #e4e4ee);border-radius:20px;padding:32px 28px;text-align:center}
.pr-top{display:inline-flex;align-items:center;gap:12px;font-size:14px;color:var(--muted, #6a6d85);margin-bottom:26px}
.pr-top button{background:none;border:0;font:inherit;color:var(--muted, #6a6d85);cursor:pointer;padding:4px}
.pr-top button.on{color:var(--ink, #16182b);font-weight:700}
.pr-sw{width:52px;height:30px;border-radius:999px;background:#dcdce8;position:relative;transition:background .3s;flex:none}
.pr-sw i{position:absolute;top:3px;inset-inline-start:3px;width:24px;height:24px;border-radius:50%;background:#fff /* qa-allow: white, ידית/סמן ולא משטח טקסט */;transition:transform .3s cubic-bezier(.2,.6,.2,1)}
.pr-sw.year{background:var(--accent, #4a3aff)}
.pr-sw.year i{transform:translateX(-22px)}
.pr-price{display:flex;justify-content:center;align-items:baseline;gap:4px;font-weight:800;line-height:1;font-size:clamp(52px,6vw,90px);direction:ltr}
.pr-digit{display:inline-block;overflow:hidden;height:1em;position:relative}
.pr-digit span{display:block;will-change:transform}
.pr-cur{font-size:.55em;font-weight:700;color:var(--muted, #6a6d85)}
.pr-sub{color:var(--muted, #6a6d85);font-size:14px;margin-top:14px;min-height:1.4em}
.pr-save{display:inline-block;margin-top:8px;font-size:12px;background:#e9ffd6;color:#2b6a13;border-radius:999px;padding:5px 12px}
.pr-save[hidden]{display:none}

HTML

<div class="stage tight"><div class="pr-card">
  <div class="pr-top">
    <button class="to-m on">חודשי</button>
    <span class="pr-sw" role="button" tabindex="0" aria-label="החלפת מסלול תשלום"><i></i></span>
    <button class="to-y">שנתי</button>
  </div>
  <div class="pr-price" aria-live="polite"><span class="pr-cur">₪</span><span class="pr-num"></span></div>
  <p class="pr-sub">לחודש, ללא התחייבות</p>
  <span class="pr-save" hidden>חיסכון של 20%</span>
</div></div>

JS

(function(){
  const PLANS={m:{price:"390",sub:"לחודש, ללא התחייבות",save:false},y:{price:"312",sub:"לחודש, בחיוב שנתי",save:true}};
  const num=document.querySelector(".pr-num"),sub=document.querySelector(".pr-sub"),save=document.querySelector(".pr-save");
  const sw=document.querySelector(".pr-sw"),bm=document.querySelector(".to-m"),by=document.querySelector(".to-y");
  let plan="m",digits=[];
  function build(str){
    num.innerHTML="";
    digits=[...str].map(c=>{
      const w=document.createElement("span");w.className="pr-digit";
      const inner=document.createElement("span");inner.textContent=c;
      w.appendChild(inner);num.appendChild(w);return inner;
    });
  }
  function roll(str){
    if(digits.length!==str.length){build(str);return;}
    [...str].forEach((c,i)=>{
      const el=digits[i];
      if(el.textContent===c)return;
      gsap.timeline({delay:i*0.06})
        .to(el,{yPercent:-110,autoAlpha:0,duration:.22,ease:"power2.in",onComplete:()=>el.textContent=c})
        .fromTo(el,{yPercent:110,autoAlpha:0},{yPercent:0,autoAlpha:1,duration:.34,ease:"back.out(2)"});
    });
  }
  function apply(next){
    plan=next;const p=PLANS[plan];
    roll(p.price);
    sub.textContent=p.sub;save.hidden=!p.save;
    sw.classList.toggle("year",plan==="y");
    bm.classList.toggle("on",plan==="m");by.classList.toggle("on",plan==="y");
  }
  build(PLANS.m.price);
  const toggle=()=>apply(plan==="m"?"y":"m");
  sw.addEventListener("click",toggle);
  sw.addEventListener("keydown",e=>{if(e.key===" "||e.key==="Enter"){e.preventDefault();toggle();}});
  bm.addEventListener("click",()=>apply("m"));
  by.addEventListener("click",()=>apply("y"));
})();