כפתור שהופך

ברגע של ההתחייבות (שליחה, הזמנה, תשלום) הכפתור עצמו עונה. זה אותו אלמנט לאורך כל הדרך: גלולה, עיגול עם ספינר, וגלולה ירוקה עם וי. הרוחב של כל מצב נמדד מראש, כי רוחב auto לא עובר אנימציה, והתוכן מתחלף עם טשטוש קצר משלו כדי שטקסטים לא יעלו זה על זה. aria-busy ו-aria-live מכריזים על המצב. כפתור שהופך הוא רכיב מתוך רכיבים של כליקס. מה שמורידים הוא רכיב שלם, גם כ-HTML עצמאי וגם כרכיב React. אין ספרייה חיצונית, אין שלב בנייה, ואם כליקס ייעלם מחר הקוד ימשיך לעבוד. מתאים לעמודים ולמסמכים שעוסקים בטפסים, כפתור, מיקרו-אינטראקציה ובלי ספריות. באותה סדרה יש גם שאלות נפוצות, ציר תהליך וקרוסלת המלצות.

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

<!-- כפתור שהופך: לוחצים, הוא מתכווץ לטעינה, ואז נפתח לאישור.
     בקוד שלכם: klixMorph.done(btn) כשהשליחה הצליחה, klixMorph.reset(btn) כשנכשלה.
     data-auto="1400" מדמה שליחה שנגמרת אחרי 1.4 שניות (להדגמה בלבד) -->
<style>
.klix-morph{position:relative;display:inline-flex;align-items:center;justify-content:center;height:3rem;
  padding:0;border:0;border-radius:999px;background:#4f46e5;color:#fff;font:inherit;font-weight:700;cursor:pointer;
  overflow:hidden;transition:width .45s cubic-bezier(.34,1.2,.64,1),background-color .3s ease}
.klix-morph[data-state="done"]{background:#059669}
.klix-morph>span{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:.4rem;
  white-space:nowrap;transition:opacity .18s ease,filter .18s ease,transform .3s cubic-bezier(.34,1.2,.64,1)}
.klix-morph .klix-morph-idle{position:relative;padding:0 1.6rem}
.klix-morph:not([data-state="idle"]) .klix-morph-idle,
.klix-morph:not([data-state="loading"]) .klix-morph-wait,
.klix-morph:not([data-state="done"]) .klix-morph-ok{opacity:0;filter:blur(4px);transform:scale(.85)}
.klix-morph-wait i{width:1.25rem;height:1.25rem;border:2.5px solid rgba(255,255,255,.35);border-top-color:#fff;
  border-radius:50%;animation:klix-morph-spin .7s linear infinite}
.klix-morph-ok svg{width:1.2rem;height:1.2rem}
.klix-morph-ok path{stroke-dasharray:24;stroke-dashoffset:24;transition:stroke-dashoffset .35s ease .15s}
.klix-morph[data-state="done"] .klix-morph-ok path{stroke-dashoffset:0}
@keyframes klix-morph-spin{to{transform:rotate(360deg)}}
html[data-motion="off"] .klix-morph,html[data-motion="off"] .klix-morph>span,
html[data-motion="off"] .klix-morph-ok path{transition:none}
</style>

<button type="button" class="klix-morph" data-state="idle" data-done="נשלח" data-auto="1400">
  <span class="klix-morph-idle">שליחה</span>
  <span class="klix-morph-wait" aria-hidden="true"><i></i></span>
  <span class="klix-morph-ok" aria-hidden="true">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>
    <b class="klix-morph-done-text"></b>
  </span>
</button>

<script>
(function () {
  function widthOf(btn, el) {
    // מודדים מחוץ לזרימה, כי השכבות יושבות אחת על השנייה
    var clone = el.cloneNode(true);
    clone.style.cssText = 'position:absolute;visibility:hidden;inset:auto;padding:0 1.6rem;white-space:nowrap';
    btn.appendChild(clone);
    var w = clone.scrollWidth;
    btn.removeChild(clone);
    return Math.ceil(w);
  }

  function set(btn, state) {
    btn.setAttribute('data-state', state);
    btn.setAttribute('aria-busy', state === 'loading' ? 'true' : 'false');
    btn.style.width = (state === 'loading' ? btn.offsetHeight : btn._klixW[state]) + 'px';
    if (state === 'done') btn.setAttribute('aria-label', btn.getAttribute('data-done') || '');
    else btn.removeAttribute('aria-label');
  }

  window.klixMorph = {
    done: function (btn) {
      set(btn, 'done');
      setTimeout(function () { set(btn, 'idle'); }, Number(btn.getAttribute('data-reset') || 2400));
    },
    reset: function (btn) { set(btn, 'idle'); }
  };

  document.querySelectorAll('.klix-morph').forEach(function (btn) {
    var text = btn.querySelector('.klix-morph-done-text');
    if (text) text.textContent = btn.getAttribute('data-done') || 'נשלח';
    btn._klixW = {
      idle: widthOf(btn, btn.querySelector('.klix-morph-idle')),
      done: widthOf(btn, btn.querySelector('.klix-morph-ok'))
    };
    btn.style.width = btn._klixW.idle + 'px';

    btn.addEventListener('click', function () {
      if (btn.getAttribute('data-state') !== 'idle') return;
      set(btn, 'loading');
      var auto = Number(btn.getAttribute('data-auto') || 0);
      if (auto) setTimeout(function () { window.klixMorph.done(btn); }, auto);
    });
  });
})();
</script>