לשוניות עם מחוון נוזלי

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

רכיב · לשוניות · מחירון · מיקרו-אינטראקציה · בלי ספריות

<!-- לשוניות עם מחוון נוזלי. בוחרים לשונית: האירוע klix-tab נורה על הקופסה עם detail.index -->
<style>
.klix-ltabs{position:relative;display:inline-flex;gap:.25rem;padding:.3rem;border-radius:999px;background:#1e293b}
.klix-ltabs button{position:relative;z-index:1;padding:.55rem 1.1rem;border:0;border-radius:999px;background:none;
  color:#94a3b8;font:inherit;font-weight:700;cursor:pointer;transition:color .2s ease}
.klix-ltabs button[aria-selected="true"]{color:#0f172a}
.klix-ltabs-ind{position:absolute;top:.3rem;bottom:.3rem;left:0;width:0;border-radius:999px;background:#fff;
  will-change:transform,width}
</style>

<div class="klix-ltabs" role="tablist" aria-label="תקופת חיוב">
  <button type="button" role="tab" aria-selected="true">חודשי</button>
  <button type="button" role="tab" aria-selected="false">שנתי</button>
  <button type="button" role="tab" aria-selected="false">לעסקים</button>
  <span class="klix-ltabs-ind" aria-hidden="true"></span>
</div>

<script>
(function () {
  document.querySelectorAll('.klix-ltabs').forEach(function (box) {
    var tabs = Array.prototype.slice.call(box.querySelectorAll('[role="tab"]'));
    var ind = box.querySelector('.klix-ltabs-ind');
    var a = { x: 0, v: 0, to: 0 }, b = { x: 0, v: 0, to: 0 };
    var raf = 0, last = 0, kA = 300, kB = 300;

    function paint() {
      ind.style.transform = 'translateX(' + a.x + 'px)';
      ind.style.width = (b.x - a.x) + 'px';
    }

    function off() {
      return document.documentElement.getAttribute('data-motion') === 'off';
    }

    function tick(now) {
      // dt חסום משני הצדדים: שלילי או ענק היה מעיף את המחוון
      var dt = Math.min(Math.max((now - last) / 1000, 0), 1 / 30);
      last = now;
      [[a, kA], [b, kB]].forEach(function (p) {
        var s = p[0], k = p[1], damping = 2 * Math.sqrt(k) * 0.82;
        s.v += (k * (s.to - s.x) - damping * s.v) * dt;
        s.x += s.v * dt;
      });
      paint();
      var moving = [a, b].some(function (s) { return Math.abs(s.to - s.x) > 0.3 || Math.abs(s.v) > 0.3; });
      raf = moving ? requestAnimationFrame(tick) : 0;
    }

    function select(i, animate) {
      tabs.forEach(function (t, j) {
        t.setAttribute('aria-selected', j === i ? 'true' : 'false');
        t.tabIndex = j === i ? 0 : -1;
      });
      var t = tabs[i];
      var toA = t.offsetLeft, toB = t.offsetLeft + t.offsetWidth;
      if (!animate || off()) {
        a.x = a.to = toA; b.x = b.to = toB; a.v = b.v = 0; paint(); return;
      }
      // הקצה שבכיוון התנועה מוביל: קשיח יותר, ולכן המחוון נמתח
      var right = toB > b.x;
      kA = right ? 170 : 420; kB = right ? 420 : 170;
      a.to = toA; b.to = toB;
      if (!raf) { last = performance.now(); raf = requestAnimationFrame(tick); }
      box.dispatchEvent(new CustomEvent('klix-tab', { detail: { index: i } }));
    }

    tabs.forEach(function (t, i) {
      t.addEventListener('click', function () { select(i, true); });
    });

    box.addEventListener('keydown', function (e) {
      var rtl = getComputedStyle(box).direction === 'rtl';
      var step = e.key === 'ArrowLeft' ? (rtl ? 1 : -1) : e.key === 'ArrowRight' ? (rtl ? -1 : 1) : 0;
      if (!step) return;
      var cur = tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; });
      var next = (cur + step + tabs.length) % tabs.length;
      select(next, true);
      tabs[next].focus();
    });

    var start = tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; });
    select(start < 0 ? 0 : start, false);
    // גופן שנטען אחר כך משנה רוחב של לשוניות
    window.addEventListener('resize', function () { select(tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; }), false); });
  });
})();
</script>