ZAKAI LAB
ANIMATION LIBRARY

IDEAS
DESIGN
TECHNOLOGY
PEOPLE

MOTION
FOR A
BRIGHTER WEB

03

LAB Neural Wander

AI flows. Ideas connect.
Scroll
GSAP
Backgrounds
ZAKAI LAB
SCROLL STORIES / CHAPTER LIGHT RAIL

01 / 04 · הרעיון

כל סיפור מתחיל בנקודה.

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

הנקודה מוצאת כיוון.

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

ת כיוון.

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

ממשיכות בתנועה

03 / התנועה

הכיוון
הופך לתנועה.

אפשר לגלול קדימה ואחורה, או ללחוץ על אחת התחנות כדי להגיע ישר לפרק שרוצות לקרוא.

אל החיבור

 

04 / החיבור

והתנועה
מחברת הכול.

המסילה מסיימת בפרק האחרון. אזור הקודים שמתחת לדוגמה אינו משנה את ההתקדמות של הסיפור.

בחזרה לרעיון

				
					<script>
document.addEventListener('zkl:chapterchange', event => {
  document.querySelectorAll('[data-zkl-current]').forEach(el => { el.textContent = String(event.detail.index + 1).padStart(2, '0') + ' / ' + String(event.detail.total).padStart(2, '0') + ' · ' + event.detail.name; });
});
document.querySelectorAll('[data-zkl-copy]').forEach(button => button.addEventListener('click', async () => {
  const code = button.closest('details').querySelector('code');
  try { await navigator.clipboard.writeText(code.textContent); button.textContent = 'הועתק ✓'; }
  catch (_) { const range = document.createRange(); range.selectNodeContents(code); const selection = getSelection(); selection.removeAllRanges(); selection.addRange(range); button.textContent = 'הקוד סומן להעתקה'; }
}));
document.querySelectorAll('[data-zkl-close]').forEach(button => button.addEventListener('click', () => { button.closest('details').open = false; }));

(() => {
  'use strict';
  // Safe to paste once in a global footer, or below the HTML widget.
  window.ZakaiChapterRail?.destroy();
  const cleanups = [];
  let destroyed = false;
  function init() {
    if (destroyed) return;
    document.querySelectorAll('.zkl-chapter-rail').forEach(rail => {
      if (rail.dataset.ready) return;
      const rootId = rail.getAttribute('data-scroll-container');
      const scroller = rootId ? document.getElementById(rootId) : null;
      if (rootId && !scroller) return;
      // Elementor demo wrappers may have backdrop-filter/transform/overflow.
      // A page-level fixed rail must live outside those containing blocks.
      if (!scroller && rail.parentElement !== document.body) {
        document.body.appendChild(rail);
      }
      const root = scroller || window;
      const path = rail.querySelector('.zkl-chapter-path');
      const fill = rail.querySelector('.zkl-chapter-fill');
      const trail = rail.querySelector('.zkl-chapter-trail');
      const head = rail.querySelector('.zkl-chapter-head');
      if (!path || !fill || !trail || !head) return;
      const allLinks = [...rail.querySelectorAll('.zkl-chapter-stop')];
      const items = allLinks.map(link => {
        const href = link.getAttribute('href') || '';
        let section = null;
        try { section = href.startsWith('#') ? document.getElementById(decodeURIComponent(href.slice(1))) : null; } catch (_) {}
        link.hidden = !section || !!(scroller && !scroller.contains(section));
        return {link, section, top: 0};
      }).filter(item => !item.link.hidden);
      if (!items.length) { rail.hidden = true; return; }
      rail.hidden = false;
      rail.dataset.ready = 'true';
      const motion = matchMedia('(prefers-reduced-motion: reduce)');
      let length = path.getTotalLength(), dirty = true, raf = 0, current = -1;
      let offset = 0, pending = null;
      const y = () => scroller ? scroller.scrollTop : (document.scrollingElement || document.documentElement).scrollTop;
      const maxY = () => Math.max(0, scroller ? scroller.scrollHeight - scroller.clientHeight : (document.scrollingElement || document.documentElement).scrollHeight - innerHeight);
      function measure() {
        offset = Math.max(0, parseFloat(getComputedStyle(rail).getPropertyValue('--zkl-scroll-offset')) || 0);
        const origin = scroller ? scroller.getBoundingClientRect().top + scroller.clientTop : 0;
        const scroll = y(), max = maxY();
        items.forEach(item => { item.top = Math.min(max, Math.max(0, item.section.getBoundingClientRect().top - origin + scroll - offset)); });
        // Links should follow the visual order of the chapters.
        items.sort((a, b) => a.top - b.top);
        length = path.getTotalLength();
        items.forEach((item, i) => {
          const point = path.getPointAtLength(items.length === 1 ? 0 : length * i / (items.length - 1));
          item.link.style.left = `${point.x / 28 * 100}%`;
          item.link.style.top = `${point.y / 1000 * 100}%`;
        });
        dirty = false;
      }
      function finishFocus(item) {
        const heading = item.section.querySelector('[data-zkl-chapter-heading], h1, h2, h3') || item.section;
        if (!heading.hasAttribute('tabindex')) heading.setAttribute('tabindex', '-1');
        heading.focus({preventScroll: true});
      }
      function render() {
        raf = 0;
        if (dirty) measure();
        const scroll = y();
        let index = 0;
        for (let i = 1; i < items.length; i++) if (scroll >= items[i].top - 2) index = i;
        const next = items[index + 1];
        const span = next ? next.top - items[index].top : 0;
        const t = next && span > 0 ? Math.max(0, Math.min(1, (scroll - items[index].top) / span)) : 0;
        const p = items.length > 1 ? (index + t) / (items.length - 1) : 0;
        const point = path.getPointAtLength(length * p);
        head.setAttribute('cx', point.x); head.setAttribute('cy', point.y);
        fill.style.strokeDasharray = `${length} ${length}`;
        fill.style.strokeDashoffset = String(length * (1 - p));
        const tail = Math.min(75, length * p);
        trail.style.strokeDasharray = `${tail} ${length}`;
        trail.style.strokeDashoffset = String(-Math.max(0, length * p - tail));
        rail.dataset.progress = p.toFixed(4);
        if (index !== current) {
          current = index;
          items.forEach((item, i) => {
            if (i === index) item.link.setAttribute('aria-current', 'location');
            else item.link.removeAttribute('aria-current');
            item.link.classList.toggle('is-past', i < index);
          });
          rail.dataset.chapter = String(index + 1);
          const name = items[index].link.querySelector('.zkl-chapter-label')?.textContent || items[index].section.id;
          rail.dispatchEvent(new CustomEvent('zkl:chapterchange', {bubbles: true, detail: {index, total: items.length, name, id: items[index].section.id}}));
        }
        if (pending && Math.abs(scroll - pending.top) < 3) {
          finishFocus(pending); pending = null;
        }
      }
      function schedule() { if (!raf) raf = requestAnimationFrame(render); }
      function resize() { dirty = true; schedule(); }
      function cancelPending() { pending = null; }
      const clicks = items.map(item => {
        function click(event) {
          if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
          event.preventDefault(); measure(); pending = item;
          root.scrollTo({top: item.top, behavior: motion.matches ? 'instant' : 'smooth'});
          if (!scroller) { try { history.replaceState(null, '', '#' + encodeURIComponent(item.section.id)); } catch (_) {} }
          schedule();
        }
        item.link.addEventListener('click', click);
        return () => item.link.removeEventListener('click', click);
      });
      root.addEventListener('scroll', schedule, {passive: true});
      root.addEventListener('wheel', cancelPending, {passive: true});
      root.addEventListener('touchstart', cancelPending, {passive: true});
      addEventListener('resize', resize);
      addEventListener('pageshow', resize);
      motion.addEventListener('change', resize);
      const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
      observer?.observe(scroller || document.body);
      items.forEach(item => observer?.observe(item.section));
      document.fonts?.ready.then(() => { if (!destroyed) resize(); });
      render();
      cleanups.push(() => {
        cancelAnimationFrame(raf); observer?.disconnect(); clicks.forEach(fn => fn());
        root.removeEventListener('scroll', schedule);
        root.removeEventListener('wheel', cancelPending);
        root.removeEventListener('touchstart', cancelPending);
        removeEventListener('resize', resize); removeEventListener('pageshow', resize);
        motion.removeEventListener('change', resize); delete rail.dataset.ready;
      });
    });
  }
  window.ZakaiChapterRail = {
    init,
    refresh() { dispatchEvent(new Event('resize')); },
    destroy() { destroyed = true; document.removeEventListener('DOMContentLoaded', init); cleanups.splice(0).forEach(fn => fn()); }
  };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, {once: true});
  else init();
})();
</script>
				
			
				
					/* Scoped component. Existing LAB pages: keep the 182px header offset. */
.zkl-chapter-rail {
  --zkl-rail-width: 56px;
  --zkl-rail-top: 182px;
  --zkl-rail-bottom: 76px;
  --zkl-scroll-offset: 182px;
  --zkl-gold: #dda532;
  --zkl-light: #ffe6a1;
  --zkl-rail-surface: #28221e;
  position: fixed; left: 24px; top: 0; bottom: 0; width: 72px;
  z-index: 90; pointer-events: none; direction: ltr;
  font-family: inherit;
}
.zkl-chapter-rail[hidden], .zkl-chapter-stop[hidden] { display: none; }
.zkl-chapter-rail::before {
  content: ""; position: absolute; top: calc(var(--zkl-rail-top) - 52px); bottom: 24px; left: 0; right: 0;
  border-radius: 36px;
  background: linear-gradient(180deg, #483a29, #2d241c 52%, #3b2f21);
  border: 1px solid #d9ae6370; box-shadow: 0 8px 32px #00000045, inset 0 0 22px #e3b96812;
}
.zkl-chapter-caption { position: absolute; top: calc(var(--zkl-rail-top) - 36px); left: 0; right: 0; text-align: center; font-size: 12px; color: #f0d29a; direction: rtl; }
.zkl-chapter-canvas {
  position: absolute; left: 8px; top: var(--zkl-rail-top); bottom: var(--zkl-rail-bottom);
  width: var(--zkl-rail-width); min-height: 100px;
}
.zkl-chapter-svg { width: 100%; height: 100%; overflow: visible; display: block; }
.zkl-chapter-path, .zkl-chapter-fill, .zkl-chapter-trail { fill: none; }
.zkl-chapter-path { stroke: #d1ad6b9e; stroke-width: 1.25; }
.zkl-chapter-fill { stroke: var(--zkl-gold); stroke-width: 1.8; opacity: .9; }
.zkl-chapter-trail { stroke: var(--zkl-light); stroke-width: 2.6; stroke-linecap: round; filter: drop-shadow(0 0 5px #dda532); }
.zkl-chapter-head { fill: var(--zkl-light); filter: drop-shadow(0 0 4px #dda532); }
.zkl-chapter-stop {
  position: absolute; left: 14px; width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  transform: translate(-50%, -50%); pointer-events: auto;
  color: #c5b7a6; text-decoration: none; -webkit-tap-highlight-color: transparent;
  font-size: 12px; font-weight: 500;
}
.zkl-chapter-stop:nth-of-type(1) { top: 1.8%; }
.zkl-chapter-stop:nth-of-type(2) { top: 34%; }
.zkl-chapter-stop:nth-of-type(3) { top: 66%; }
.zkl-chapter-stop:nth-of-type(4) { top: 98.2%; }
.zkl-chapter-dot {
  box-sizing: border-box; width: 34px; height: 34px; border: 2px solid #c1a16c;
  border-radius: 50%; background: var(--zkl-rail-surface);
  display: grid; place-items: center; transition: background .2s, color .2s, border-color .2s;
}
.zkl-chapter-stop.is-past .zkl-chapter-dot { border-color: var(--zkl-gold); color: #f0ca79; }
.zkl-chapter-stop[aria-current="location"] .zkl-chapter-dot {
  background: var(--zkl-light); color: #2e2417; border-color: var(--zkl-light);
  box-shadow: 0 0 0 5px #dda53226, 0 0 18px #e4b44f66;
}
.zkl-chapter-stop:hover .zkl-chapter-dot { border-color: var(--zkl-light); }
.zkl-chapter-stop:focus-visible { outline: 2px solid var(--zkl-light); outline-offset: -1px; border-radius: 50%; }
.zkl-chapter-label {
  position: absolute; left: 52px; white-space: nowrap; direction: rtl;
  font-size: 12px; line-height: 1.3; font-weight: 400;
  color: #f4e5cb; background: #28221ef5; border: 1px solid #826642;
  padding: 7px 11px; border-radius: 999px;
  opacity: 1; transform: none; transition: opacity .2s, transform .2s;
  pointer-events: none;
}
.zkl-chapter-stop:hover .zkl-chapter-label,
.zkl-chapter-stop:focus-visible .zkl-chapter-label,
.zkl-chapter-stop[aria-current="location"] .zkl-chapter-label { opacity: 1; transform: none; }
@media (max-width: 767px) {
  .zkl-chapter-rail { --zkl-rail-width: 36px; --zkl-rail-top: 110px; --zkl-rail-bottom: 76px; left: 12px; width: 52px; }
  .zkl-chapter-dot { width: 30px; height: 30px; }
  .zkl-chapter-caption { font-size: 11px; }
  .zkl-chapter-label { opacity: 0; }
  .zkl-chapter-stop[aria-current="location"] .zkl-chapter-label { opacity: 0; }
  .zkl-chapter-stop:hover .zkl-chapter-label,
  .zkl-chapter-stop:focus-visible .zkl-chapter-label { opacity: 1; }
}
@media (max-height: 500px) {
  .zkl-chapter-rail { --zkl-rail-top: 80px; --zkl-rail-bottom: 35px; }
}
@media (prefers-reduced-motion: reduce) {
  .zkl-chapter-stop, .zkl-chapter-dot, .zkl-chapter-label { transition: none; }
  .zkl-chapter-trail { display: none; }
  .zkl-chapter-head { filter: none; }
}

*{box-sizing:border-box}html{scroll-behavior:auto}body{margin:0;background:#181511;color:#f2eadf;font-family:Arial,Heebo,sans-serif}button,a{-webkit-tap-highlight-color:transparent}button{font:inherit}a{color:#e8bb67}*:focus-visible{outline:2px solid #ffe6a1;outline-offset:5px}
.zkl-demo-header{position:fixed;inset:0 0 auto 0;height:78px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 clamp(22px,4vw,64px);background:#181511ed;border-bottom:1px solid #e7c58a20;z-index:100}.zkl-demo-brand{font-size:18px;letter-spacing:3px;direction:ltr;font-weight:400}.zkl-demo-status{font-size:13px;color:#d8c3a5}.zkl-demo-header small{color:#a89a89;font-size:11px;letter-spacing:1.5px}
.zkl-demo-main{padding-top:78px}.zkl-demo-chapter{min-height:56svh;padding:clamp(30px,4vw,52px) clamp(25px,6vw,90px) 38px 170px;display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);align-items:center;gap:36px;border-bottom:1px solid #e7c58a19;background:radial-gradient(ellipse at 25% 35%,#44321e45,transparent 65%)}.zkl-demo-chapter:nth-child(2){background:#201b16}.zkl-demo-chapter:nth-child(3){background:radial-gradient(ellipse at 25% 50%,#5e42284d,transparent 60%),#171410}.zkl-demo-chapter:nth-child(4){background:#211d17}.zkl-demo-kicker{color:#dab66d;font-size:12px;letter-spacing:1px}.zkl-demo-chapter h1,.zkl-demo-chapter h2{font-weight:400;font-size:clamp(32px,3.8vw,52px);line-height:1.14;letter-spacing:-2px;margin:18px 0}.zkl-demo-chapter p{color:#c5b7a6;max-width:360px;font-size:15px;line-height:1.6}.zkl-demo-next{display:inline-flex;align-items:center;gap:14px;text-decoration:none;padding:14px 0;color:#e8bb67;font-size:13px;border-bottom:1px solid #816638}
.zkl-art{aspect-ratio:1;overflow:clip;position:relative;max-width:210px;width:100%;margin:auto}.zkl-art-ring{position:absolute;inset:8%;border:1px solid #c3984d80;border-radius:50%}.zkl-art-ring::before{content:"";position:absolute;width:14px;height:14px;top:11%;left:11%;border-radius:50%;background:#f2d491;box-shadow:0 0 30px #e6a54080}.zkl-art-ring::after{content:"";position:absolute;inset:18%;border:1px solid #c3984d30;border-radius:50%}.zkl-art-center{position:absolute;inset:0;display:grid;place-items:center;direction:ltr;font-size:34px;font-weight:400;color:#d8b87a55;letter-spacing:8px}.zkl-art-lines{position:absolute;inset:15%;display:flex;gap:24px;align-items:center;transform:rotate(-25deg)}.zkl-art-lines span{height:80%;width:1px;background:linear-gradient(transparent,#e8c88d,transparent);position:relative;flex:1}.zkl-art-lines span::after{content:"";height:10px;width:10px;position:absolute;top:30%;left:-4px;background:#f0d18d;border-radius:50%;box-shadow:0 0 20px #dda53277}.zkl-art-lines span:nth-child(2)::after{top:65%}.zkl-art-lines span:nth-child(3)::after{top:45%}.zkl-art-double .zkl-art-ring{inset:5% 15%;transform:rotate(-30deg)}.zkl-art-double .zkl-art-ring::after{inset:-8%;transform:rotate(60deg);border-color:#c3984d80}.zkl-art-grid{position:absolute;inset:20%;display:grid;grid-template-columns:1fr 1fr;gap:24px;transform:rotate(-12deg)}.zkl-art-grid span{border:1px solid #d8b16c80;border-radius:50%}.zkl-art-grid span:last-child{background:radial-gradient(circle,#e4c98d,#a98242);box-shadow:0 0 40px #dda5322b}
.zkl-demo-guide{overflow-wrap:anywhere;padding:60px clamp(24px,6vw,80px) 70px 100px;max-width:1080px;margin:auto;color:#cebfab;font-size:15px;line-height:1.7}.zkl-demo-guide h2{color:#f2eadf;font-weight:400;font-size:28px}.zkl-demo-guide h3{font-weight:400;color:#e6c38b}.zkl-demo-guide details{margin:12px 0;background:#211c17;border:1px solid #564431;border-radius:12px;padding:16px}.zkl-demo-guide summary{cursor:pointer;color:#f2e0c3;font-size:16px}.zkl-demo-guide pre{padding:18px;background:#12100e;color:#e2d5c1;white-space:pre-wrap;overflow-wrap:anywhere;overflow:auto;max-height:380px;text-align:left;direction:ltr;font:12px/1.6 monospace;border:none}.zkl-demo-guide button{margin:14px 0 0 8px;background:#30251a;color:#f2d491;border:1px solid #8b6a3c;border-radius:999px;padding:9px 15px;cursor:pointer;min-height:42px}.zkl-demo-guide code{direction:ltr;unicode-bidi:embed}.zkl-demo-guide table{width:100%;border-collapse:collapse}.zkl-demo-guide td,.zkl-demo-guide th{padding:10px;border-bottom:1px solid #554430;text-align:right}.zkl-demo-guide ul{padding-inline-start:22px}
.zkl-chapter-rail{--zkl-rail-top:max(140px,25dvh);--zkl-rail-bottom:25dvh;--zkl-scroll-offset:96px}
@media(max-width:767px){.zkl-demo-header{padding:0 18px;height:70px;gap:10px}.zkl-demo-brand{font-size:13px;letter-spacing:1.8px}.zkl-demo-header small{display:none}.zkl-demo-status{font-size:12px}.zkl-demo-main{padding-top:70px}.zkl-demo-chapter{padding:32px 22px 32px 88px;grid-template-columns:1fr;gap:12px;min-height:54svh}.zkl-demo-chapter h1,.zkl-demo-chapter h2{font-size:clamp(30px,7vw,38px);letter-spacing:-1px;margin:20px 0}.zkl-demo-chapter p{font-size:15px}.zkl-art{max-width:135px}.zkl-chapter-rail{--zkl-rail-top:max(130px,23dvh);--zkl-scroll-offset:86px;--zkl-rail-bottom:25dvh}.zkl-demo-guide{overflow-wrap:anywhere;padding:40px 22px 50px 86px}.zkl-demo-guide table{font-size:12px}.zkl-demo-guide td,.zkl-demo-guide th{padding:8px 3px}}
@media(max-height:500px){.zkl-chapter-rail{--zkl-rail-top:100px;--zkl-rail-bottom:70px}}
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
				
			
				
					<div><strong class="zkl-demo-brand">ZAKAI LAB</strong><br><small>SCROLL STORIES / CHAPTER LIGHT RAIL</small></div><span class="zkl-demo-status" dir="ltr" data-zkl-current>01 / 04 · הרעיון</span></header><nav class="zkl-chapter-rail" aria-label="ניווט בין פרקי העמוד">
  <span class="zkl-chapter-caption" aria-hidden="true">פרקים</span>
  <div class="zkl-chapter-canvas">
    <svg class="zkl-chapter-svg" viewBox="0 0 28 1000" preserveAspectRatio="none" aria-hidden="true">
      <path class="zkl-chapter-path" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <path class="zkl-chapter-fill" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <path class="zkl-chapter-trail" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <circle class="zkl-chapter-head" cx="14" cy="18" r="3.5" />
    </svg>
    <a class="zkl-chapter-stop" href="#lab-story-01" aria-current="location"><span class="zkl-chapter-dot">01</span><span class="zkl-chapter-label">הרעיון</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-02"><span class="zkl-chapter-dot">02</span><span class="zkl-chapter-label">הכיוון</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-03"><span class="zkl-chapter-dot">03</span><span class="zkl-chapter-label">התנועה</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-04"><span class="zkl-chapter-dot">04</span><span class="zkl-chapter-label">החיבור</span></a>
  </div>
</nav>
<main class="zkl-demo-main">
<section class="zkl-demo-chapter" id="lab-story-01"><div><span class="zkl-demo-kicker">01 / הרעיון</span><h1>כל סיפור<br>מתחיל בנקודה.</h1><p>גללי מטה. האור במסילה מתקדם עם הסיפור, והתחנה הפעילה מסמנת היכן אנחנו נמצאים.</p><a class="zkl-demo-next" href="#lab-story-02">אל הפרק הבא <span aria-hidden="true">↓</span></a></div><div class="zkl-art" aria-hidden="true"><div class="zkl-art-ring"></div><div class="zkl-art-center">Z</div></div></section>
<section class="zkl-demo-chapter" id="lab-story-02"><div><span class="zkl-demo-kicker">02 / הכיוון</span><h2>הנקודה<br>מוצאת כיוון.</h2><p>המרחק בין התחנות מייצג מעבר בין פרקים. האור מתקדם בהתאם למיקום האמיתי שלהם בעמוד.</p><a class="zkl-demo-next" href="#lab-story-03">ממשיכות בתנועה <span aria-hidden="true">↓</span></a></div><div class="zkl-art" aria-hidden="true"><div class="zkl-art-lines"><span></span><span></span><span></span></div></div></section>
<section class="zkl-demo-chapter" id="lab-story-03"><div><span class="zkl-demo-kicker">03 / התנועה</span><h2>הכיוון<br>הופך לתנועה.</h2><p>אפשר לגלול קדימה ואחורה, או ללחוץ על אחת התחנות כדי להגיע ישר לפרק שרוצות לקרוא.</p><a class="zkl-demo-next" href="#lab-story-04">אל החיבור <span aria-hidden="true">↓</span></a></div><div class="zkl-art zkl-art-double" aria-hidden="true"><div class="zkl-art-ring"></div><div class="zkl-art-center">03</div></div></section>
<section class="zkl-demo-chapter" id="lab-story-04"><div><span class="zkl-demo-kicker">04 / החיבור</span><h2>והתנועה<br>מחברת הכול.</h2><p>המסילה מסיימת בפרק האחרון. אזור הקודים שמתחת לדוגמה אינו משנה את ההתקדמות של הסיפור.</p><a class="zkl-demo-next" href="#lab-story-01">בחזרה לרעיון <span aria-hidden="true">↑</span></a></div><div class="zkl-art" aria-hidden="true"><div class="zkl-art-grid"><span></span><span></span><span></span><span></span></div></div></section>
</main>
<section class="zkl-demo-guide" id="installation"><h2>התקנה ב־Elementor</h2>
<p>הרכיב עצמאי ופועל ללא GSAP או ספריות חיצוניות. אפשר להשתמש בו לניווט בין פרקי סיפור, קטגוריות או מקטעים בעמוד.</p>
<ol><li>בקונטיינרים של ארבעת הפרקים הגדירי <strong>CSS ID</strong> לפי הטבלה. ללא הסימן #.</li><li>הוסיפי ווידג׳ט HTML אחד והדביקי את קוד ה־HTML שבתיבה הראשונה.</li><li>הוסיפי את ה־CSS לאזור הסטייל של עמוד ההדגמה או לקוד הגלובלי של ה־LAB.</li><li>הדביקי את ה־JS שבתיבה השלישית פעם אחת, לאחר ה־HTML. תגי script כבר כלולים.</li><li>התאימי את <code>--zkl-scroll-offset</code> לגובה ההדר ולמרווח שמעל התוכן. ברירת המחדל לרכיב היא 182px; בדמו הזה ההדר קטן יותר.</li></ol>
<table><thead><tr><th>פרק</th><th>CSS ID של הקונטיינר</th><th>קישור התחנה</th></tr></thead><tbody>
<tr><td>הרעיון</td><td><code>lab-story-01</code></td><td><code>#lab-story-01</code></td></tr>
<tr><td>הכיוון</td><td><code>lab-story-02</code></td><td><code>#lab-story-02</code></td></tr>
<tr><td>התנועה</td><td><code>lab-story-03</code></td><td><code>#lab-story-03</code></td></tr>
<tr><td>החיבור</td><td><code>lab-story-04</code></td><td><code>#lab-story-04</code></td></tr></tbody></table>
				
			

הערה חשובה  🚩  

הרכיב פועל ללא ספריות חיצוניות. יש להוסיף את קוד ה־JavaScript פעם אחת, לאחר קוד ה־HTML של הרכיב.

ZAKAI LAB
SCROLL STORIES / CHAPTER LIGHT RAIL
01 / 04 · הרעיון
01 / הרעיון

כל סיפור
מתחיל בנקודה.

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

אל הפרק הבא
02 / הכיוון

הנקודה
מוצאת כיוון.

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

ממשיכות בתנועה
03 / התנועה

הכיוון
הופך לתנועה.

אפשר לגלול קדימה ואחורה, או ללחוץ על אחת התחנות כדי להגיע ישר לפרק שרוצות לקרוא.

אל החיבור
04 / החיבור

והתנועה
מחברת הכול.

המסילה מסיימת בפרק האחרון. אזור הקודים שמתחת לדוגמה אינו משנה את ההתקדמות של הסיפור.

בחזרה לרעיון

התקנה ב־Elementor

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

  1. בקונטיינרים של ארבעת הפרקים הגדירי CSS ID לפי הטבלה. ללא הסימן #.
  2. הוסיפי ווידג׳ט HTML אחד והדביקי את קוד ה־HTML שבתיבה הראשונה.
  3. הוסיפי את ה־CSS לאזור הסטייל של עמוד ההדגמה או לקוד הגלובלי של ה־LAB.
  4. הדביקי את ה־JS שבתיבה השלישית פעם אחת, לאחר ה־HTML. תגי script כבר כלולים.
  5. התאימי את --zkl-scroll-offset לגובה ההדר ולמרווח שמעל התוכן. ברירת המחדל לרכיב היא 182px; בדמו הזה ההדר קטן יותר.
פרקCSS ID של הקונטיינרקישור התחנה
הרעיוןlab-story-01#lab-story-01
הכיווןlab-story-02#lab-story-02
התנועהlab-story-03#lab-story-03
החיבורlab-story-04#lab-story-04

ב־LAB השתמשי במסילה החדשה במקום המסילה הדקורטיבית הישנה ובמקום סקריפט האור הישן. תפריט הצד הוא רכיב נפרד. בדקי שמיקום לשונית פתיחת התפריט אינו מכסה תחנה; אפשר לשנות את תחילת המסלול ואת סופו במשתנים שלמטה.

קודים להעתקה

HTML
<nav class="zkl-chapter-rail" aria-label="ניווט בין פרקי העמוד">
  <span class="zkl-chapter-caption" aria-hidden="true">פרקים</span>
  <div class="zkl-chapter-canvas">
    <svg class="zkl-chapter-svg" viewBox="0 0 28 1000" preserveAspectRatio="none" aria-hidden="true">
      <path class="zkl-chapter-path" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <path class="zkl-chapter-fill" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <path class="zkl-chapter-trail" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <circle class="zkl-chapter-head" cx="14" cy="18" r="3.5" />
    </svg>
    <a class="zkl-chapter-stop" href="#lab-story-01" aria-current="location"><span class="zkl-chapter-dot">01</span><span class="zkl-chapter-label">הרעיון</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-02"><span class="zkl-chapter-dot">02</span><span class="zkl-chapter-label">הכיוון</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-03"><span class="zkl-chapter-dot">03</span><span class="zkl-chapter-label">התנועה</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-04"><span class="zkl-chapter-dot">04</span><span class="zkl-chapter-label">החיבור</span></a>
  </div>
</nav>
CSS
/* Scoped component. Existing LAB pages: keep the 182px header offset. */
.zkl-chapter-rail {
  --zkl-rail-width: 56px;
  --zkl-rail-top: 182px;
  --zkl-rail-bottom: 76px;
  --zkl-scroll-offset: 182px;
  --zkl-gold: #dda532;
  --zkl-light: #ffe6a1;
  --zkl-rail-surface: #28221e;
  position: fixed; left: 24px; top: 0; bottom: 0; width: 72px;
  z-index: 90; pointer-events: none; direction: ltr;
  font-family: inherit;
}
.zkl-chapter-rail[hidden], .zkl-chapter-stop[hidden] { display: none; }
.zkl-chapter-rail::before {
  content: ""; position: absolute; top: calc(var(--zkl-rail-top) - 52px); bottom: 24px; left: 0; right: 0;
  border-radius: 36px;
  background: linear-gradient(180deg, #483a29, #2d241c 52%, #3b2f21);
  border: 1px solid #d9ae6370; box-shadow: 0 8px 32px #00000045, inset 0 0 22px #e3b96812;
}
.zkl-chapter-caption { position: absolute; top: calc(var(--zkl-rail-top) - 36px); left: 0; right: 0; text-align: center; font-size: 12px; color: #f0d29a; direction: rtl; }
.zkl-chapter-canvas {
  position: absolute; left: 8px; top: var(--zkl-rail-top); bottom: var(--zkl-rail-bottom);
  width: var(--zkl-rail-width); min-height: 100px;
}
.zkl-chapter-svg { width: 100%; height: 100%; overflow: visible; display: block; }
.zkl-chapter-path, .zkl-chapter-fill, .zkl-chapter-trail { fill: none; }
.zkl-chapter-path { stroke: #d1ad6b9e; stroke-width: 1.25; }
.zkl-chapter-fill { stroke: var(--zkl-gold); stroke-width: 1.8; opacity: .9; }
.zkl-chapter-trail { stroke: var(--zkl-light); stroke-width: 2.6; stroke-linecap: round; filter: drop-shadow(0 0 5px #dda532); }
.zkl-chapter-head { fill: var(--zkl-light); filter: drop-shadow(0 0 4px #dda532); }
.zkl-chapter-stop {
  position: absolute; left: 14px; width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  transform: translate(-50%, -50%); pointer-events: auto;
  color: #c5b7a6; text-decoration: none; -webkit-tap-highlight-color: transparent;
  font-size: 12px; font-weight: 500;
}
.zkl-chapter-stop:nth-of-type(1) { top: 1.8%; }
.zkl-chapter-stop:nth-of-type(2) { top: 34%; }
.zkl-chapter-stop:nth-of-type(3) { top: 66%; }
.zkl-chapter-stop:nth-of-type(4) { top: 98.2%; }
.zkl-chapter-dot {
  box-sizing: border-box; width: 34px; height: 34px; border: 2px solid #c1a16c;
  border-radius: 50%; background: var(--zkl-rail-surface);
  display: grid; place-items: center; transition: background .2s, color .2s, border-color .2s;
}
.zkl-chapter-stop.is-past .zkl-chapter-dot { border-color: var(--zkl-gold); color: #f0ca79; }
.zkl-chapter-stop[aria-current="location"] .zkl-chapter-dot {
  background: var(--zkl-light); color: #2e2417; border-color: var(--zkl-light);
  box-shadow: 0 0 0 5px #dda53226, 0 0 18px #e4b44f66;
}
.zkl-chapter-stop:hover .zkl-chapter-dot { border-color: var(--zkl-light); }
.zkl-chapter-stop:focus-visible { outline: 2px solid var(--zkl-light); outline-offset: -1px; border-radius: 50%; }
.zkl-chapter-label {
  position: absolute; left: 52px; white-space: nowrap; direction: rtl;
  font-size: 12px; line-height: 1.3; font-weight: 400;
  color: #f4e5cb; background: #28221ef5; border: 1px solid #826642;
  padding: 7px 11px; border-radius: 999px;
  opacity: 1; transform: none; transition: opacity .2s, transform .2s;
  pointer-events: none;
}
.zkl-chapter-stop:hover .zkl-chapter-label,
.zkl-chapter-stop:focus-visible .zkl-chapter-label,
.zkl-chapter-stop[aria-current="location"] .zkl-chapter-label { opacity: 1; transform: none; }
@media (max-width: 767px) {
  .zkl-chapter-rail { --zkl-rail-width: 36px; --zkl-rail-top: 110px; --zkl-rail-bottom: 76px; left: 12px; width: 52px; }
  .zkl-chapter-dot { width: 30px; height: 30px; }
  .zkl-chapter-caption { font-size: 11px; }
  .zkl-chapter-label { opacity: 0; }
  .zkl-chapter-stop[aria-current="location"] .zkl-chapter-label { opacity: 0; }
  .zkl-chapter-stop:hover .zkl-chapter-label,
  .zkl-chapter-stop:focus-visible .zkl-chapter-label { opacity: 1; }
}
@media (max-height: 500px) {
  .zkl-chapter-rail { --zkl-rail-top: 80px; --zkl-rail-bottom: 35px; }
}
@media (prefers-reduced-motion: reduce) {
  .zkl-chapter-stop, .zkl-chapter-dot, .zkl-chapter-label { transition: none; }
  .zkl-chapter-trail { display: none; }
  .zkl-chapter-head { filter: none; }
}
JS
<script>
(() => {
  'use strict';
  // Safe to paste once in a global footer, or below the HTML widget.
  window.ZakaiChapterRail?.destroy();
  const cleanups = [];
  let destroyed = false;
  function init() {
    if (destroyed) return;
    document.querySelectorAll('.zkl-chapter-rail').forEach(rail => {
      if (rail.dataset.ready) return;
      const rootId = rail.getAttribute('data-scroll-container');
      const scroller = rootId ? document.getElementById(rootId) : null;
      if (rootId && !scroller) return;
      // Elementor demo wrappers may have backdrop-filter/transform/overflow.
      // A page-level fixed rail must live outside those containing blocks.
      if (!scroller && rail.parentElement !== document.body) {
        document.body.appendChild(rail);
      }
      const root = scroller || window;
      const path = rail.querySelector('.zkl-chapter-path');
      const fill = rail.querySelector('.zkl-chapter-fill');
      const trail = rail.querySelector('.zkl-chapter-trail');
      const head = rail.querySelector('.zkl-chapter-head');
      if (!path || !fill || !trail || !head) return;
      const allLinks = [...rail.querySelectorAll('.zkl-chapter-stop')];
      const items = allLinks.map(link => {
        const href = link.getAttribute('href') || '';
        let section = null;
        try { section = href.startsWith('#') ? document.getElementById(decodeURIComponent(href.slice(1))) : null; } catch (_) {}
        link.hidden = !section || !!(scroller && !scroller.contains(section));
        return {link, section, top: 0};
      }).filter(item => !item.link.hidden);
      if (!items.length) { rail.hidden = true; return; }
      rail.hidden = false;
      rail.dataset.ready = 'true';
      const motion = matchMedia('(prefers-reduced-motion: reduce)');
      let length = path.getTotalLength(), dirty = true, raf = 0, current = -1;
      let offset = 0, pending = null;
      const y = () => scroller ? scroller.scrollTop : (document.scrollingElement || document.documentElement).scrollTop;
      const maxY = () => Math.max(0, scroller ? scroller.scrollHeight - scroller.clientHeight : (document.scrollingElement || document.documentElement).scrollHeight - innerHeight);
      function measure() {
        offset = Math.max(0, parseFloat(getComputedStyle(rail).getPropertyValue('--zkl-scroll-offset')) || 0);
        const origin = scroller ? scroller.getBoundingClientRect().top + scroller.clientTop : 0;
        const scroll = y(), max = maxY();
        items.forEach(item => { item.top = Math.min(max, Math.max(0, item.section.getBoundingClientRect().top - origin + scroll - offset)); });
        // Links should follow the visual order of the chapters.
        items.sort((a, b) => a.top - b.top);
        length = path.getTotalLength();
        items.forEach((item, i) => {
          const point = path.getPointAtLength(items.length === 1 ? 0 : length * i / (items.length - 1));
          item.link.style.left = `${point.x / 28 * 100}%`;
          item.link.style.top = `${point.y / 1000 * 100}%`;
        });
        dirty = false;
      }
      function finishFocus(item) {
        const heading = item.section.querySelector('[data-zkl-chapter-heading], h1, h2, h3') || item.section;
        if (!heading.hasAttribute('tabindex')) heading.setAttribute('tabindex', '-1');
        heading.focus({preventScroll: true});
      }
      function render() {
        raf = 0;
        if (dirty) measure();
        const scroll = y();
        let index = 0;
        for (let i = 1; i < items.length; i++) if (scroll >= items[i].top - 2) index = i;
        const next = items[index + 1];
        const span = next ? next.top - items[index].top : 0;
        const t = next && span > 0 ? Math.max(0, Math.min(1, (scroll - items[index].top) / span)) : 0;
        const p = items.length > 1 ? (index + t) / (items.length - 1) : 0;
        const point = path.getPointAtLength(length * p);
        head.setAttribute('cx', point.x); head.setAttribute('cy', point.y);
        fill.style.strokeDasharray = `${length} ${length}`;
        fill.style.strokeDashoffset = String(length * (1 - p));
        const tail = Math.min(75, length * p);
        trail.style.strokeDasharray = `${tail} ${length}`;
        trail.style.strokeDashoffset = String(-Math.max(0, length * p - tail));
        rail.dataset.progress = p.toFixed(4);
        if (index !== current) {
          current = index;
          items.forEach((item, i) => {
            if (i === index) item.link.setAttribute('aria-current', 'location');
            else item.link.removeAttribute('aria-current');
            item.link.classList.toggle('is-past', i < index);
          });
          rail.dataset.chapter = String(index + 1);
          const name = items[index].link.querySelector('.zkl-chapter-label')?.textContent || items[index].section.id;
          rail.dispatchEvent(new CustomEvent('zkl:chapterchange', {bubbles: true, detail: {index, total: items.length, name, id: items[index].section.id}}));
        }
        if (pending && Math.abs(scroll - pending.top) < 3) {
          finishFocus(pending); pending = null;
        }
      }
      function schedule() { if (!raf) raf = requestAnimationFrame(render); }
      function resize() { dirty = true; schedule(); }
      function cancelPending() { pending = null; }
      const clicks = items.map(item => {
        function click(event) {
          if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
          event.preventDefault(); measure(); pending = item;
          root.scrollTo({top: item.top, behavior: motion.matches ? 'instant' : 'smooth'});
          if (!scroller) { try { history.replaceState(null, '', '#' + encodeURIComponent(item.section.id)); } catch (_) {} }
          schedule();
        }
        item.link.addEventListener('click', click);
        return () => item.link.removeEventListener('click', click);
      });
      root.addEventListener('scroll', schedule, {passive: true});
      root.addEventListener('wheel', cancelPending, {passive: true});
      root.addEventListener('touchstart', cancelPending, {passive: true});
      addEventListener('resize', resize);
      addEventListener('pageshow', resize);
      motion.addEventListener('change', resize);
      const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
      observer?.observe(scroller || document.body);
      items.forEach(item => observer?.observe(item.section));
      document.fonts?.ready.then(() => { if (!destroyed) resize(); });
      render();
      cleanups.push(() => {
        cancelAnimationFrame(raf); observer?.disconnect(); clicks.forEach(fn => fn());
        root.removeEventListener('scroll', schedule);
        root.removeEventListener('wheel', cancelPending);
        root.removeEventListener('touchstart', cancelPending);
        removeEventListener('resize', resize); removeEventListener('pageshow', resize);
        motion.removeEventListener('change', resize); delete rail.dataset.ready;
      });
    });
  }
  window.ZakaiChapterRail = {
    init,
    refresh() { dispatchEvent(new Event('resize')); },
    destroy() { destroyed = true; document.removeEventListener('DOMContentLoaded', init); cleanups.splice(0).forEach(fn => fn()); }
  };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, {once: true});
  else init();
})();

</script>
מה ניתן לשינוי
  • שמות ומספר הפרקים: ערכי, הוסיפי או הסירי קישורי zkl-chapter-stop ב־HTML. לכל תחנה צריך להיות קונטיינר בעל ID תואם. הסקריפט מסדר את התחנות לפי מיקום הקונטיינרים בפועל.
  • צבע האור והמסילה: המשתנים --zkl-gold, --zkl-light ו־--zkl-rail-surface.
  • רוחב המסילה: --zkl-rail-width, ברירת מחדל 56px במחשב ו־36px בנייד. שטח הלחיצה של התחנות הוא 44px.
  • מרווח עליון ותחתון למסלול: --zkl-rail-top ו־--zkl-rail-bottom. אלו מזיזים את התחנות בתוך המסילה.
  • מרווח בגלילה לפרק: --zkl-scroll-offset. אפשר להגדיר ערך שונה במחשב ובנייד.
  • מסילה ישרה: החליפי את d בשלושת המסלולים ב־M14 18 L14 982.
  • שם התחנה: כל שמות הפרקים גלויים במחשב. בנייד שמות התחנות זמינים בלחיצה ובניווט מקלדת בלי להשאיר תווית מעל התוכן.
  • תנועה מופחתת: מעבר ישיר לפרק, בלי גלילה חלקה ובלי שובל אור. סימון הפרק ממשיך לעבוד.
  • אחרי שינוי גובה התוכן: מתעדכן אוטומטית עם ResizeObserver. אם תוסף משנה את מיקום המקטעים בלי לשנות גובה, אפשר לקרוא ל־window.ZakaiChapterRail.refresh().

העמוד הוא דוגמה עצמאית. האתר החי לא נערך במסגרת ההכנה.

ZAKAI LAB
SCROLL STORIES / CHAPTER LIGHT RAIL
01 / 04 · הרעיון
01 / הרעיון

כל סיפור
מתחיל בנקודה.

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

אל הפרק הבא
02 / הכיוון

הנקודה
מוצאת כיוון.

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

ממשיכות בתנועה
03 / התנועה

הכיוון
הופך לתנועה.

אפשר לגלול קדימה ואחורה, או ללחוץ על אחת התחנות כדי להגיע ישר לפרק שרוצות לקרוא.

אל החיבור
04 / החיבור

והתנועה
מחברת הכול.

המסילה מסיימת בפרק האחרון. אזור הקודים שמתחת לדוגמה אינו משנה את ההתקדמות של הסיפור.

בחזרה לרעיון

התקנה ב־Elementor

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

  1. בקונטיינרים של ארבעת הפרקים הגדירי CSS ID לפי הטבלה. ללא הסימן #.
  2. הוסיפי ווידג׳ט HTML אחד והדביקי את קוד ה־HTML שבתיבה הראשונה.
  3. הוסיפי את ה־CSS לאזור הסטייל של עמוד ההדגמה או לקוד הגלובלי של ה־LAB.
  4. הדביקי את ה־JS שבתיבה השלישית פעם אחת, לאחר ה־HTML. תגי script כבר כלולים.
  5. התאימי את --zkl-scroll-offset לגובה ההדר ולמרווח שמעל התוכן. ברירת המחדל לרכיב היא 182px; בדמו הזה ההדר קטן יותר.
פרקCSS ID של הקונטיינרקישור התחנה
הרעיוןlab-story-01#lab-story-01
הכיווןlab-story-02#lab-story-02
התנועהlab-story-03#lab-story-03
החיבורlab-story-04#lab-story-04

ב־LAB השתמשי במסילה החדשה במקום המסילה הדקורטיבית הישנה ובמקום סקריפט האור הישן. תפריט הצד הוא רכיב נפרד. בדקי שמיקום לשונית פתיחת התפריט אינו מכסה תחנה; אפשר לשנות את תחילת המסלול ואת סופו במשתנים שלמטה.

קודים להעתקה

HTML
<nav class="zkl-chapter-rail" aria-label="ניווט בין פרקי העמוד">
  <span class="zkl-chapter-caption" aria-hidden="true">פרקים</span>
  <div class="zkl-chapter-canvas">
    <svg class="zkl-chapter-svg" viewBox="0 0 28 1000" preserveAspectRatio="none" aria-hidden="true">
      <path class="zkl-chapter-path" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <path class="zkl-chapter-fill" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <path class="zkl-chapter-trail" d="M14 18 C4 125 24 220 14 325 S4 530 14 610 S24 815 14 982" />
      <circle class="zkl-chapter-head" cx="14" cy="18" r="3.5" />
    </svg>
    <a class="zkl-chapter-stop" href="#lab-story-01" aria-current="location"><span class="zkl-chapter-dot">01</span><span class="zkl-chapter-label">הרעיון</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-02"><span class="zkl-chapter-dot">02</span><span class="zkl-chapter-label">הכיוון</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-03"><span class="zkl-chapter-dot">03</span><span class="zkl-chapter-label">התנועה</span></a>
    <a class="zkl-chapter-stop" href="#lab-story-04"><span class="zkl-chapter-dot">04</span><span class="zkl-chapter-label">החיבור</span></a>
  </div>
</nav>
CSS
/* Scoped component. Existing LAB pages: keep the 182px header offset. */
.zkl-chapter-rail {
  --zkl-rail-width: 56px;
  --zkl-rail-top: 182px;
  --zkl-rail-bottom: 76px;
  --zkl-scroll-offset: 182px;
  --zkl-gold: #dda532;
  --zkl-light: #ffe6a1;
  --zkl-rail-surface: #28221e;
  position: fixed; left: 24px; top: 0; bottom: 0; width: 72px;
  z-index: 90; pointer-events: none; direction: ltr;
  font-family: inherit;
}
.zkl-chapter-rail[hidden], .zkl-chapter-stop[hidden] { display: none; }
.zkl-chapter-rail::before {
  content: ""; position: absolute; top: calc(var(--zkl-rail-top) - 52px); bottom: 24px; left: 0; right: 0;
  border-radius: 36px;
  background: linear-gradient(180deg, #483a29, #2d241c 52%, #3b2f21);
  border: 1px solid #d9ae6370; box-shadow: 0 8px 32px #00000045, inset 0 0 22px #e3b96812;
}
.zkl-chapter-caption { position: absolute; top: calc(var(--zkl-rail-top) - 36px); left: 0; right: 0; text-align: center; font-size: 12px; color: #f0d29a; direction: rtl; }
.zkl-chapter-canvas {
  position: absolute; left: 8px; top: var(--zkl-rail-top); bottom: var(--zkl-rail-bottom);
  width: var(--zkl-rail-width); min-height: 100px;
}
.zkl-chapter-svg { width: 100%; height: 100%; overflow: visible; display: block; }
.zkl-chapter-path, .zkl-chapter-fill, .zkl-chapter-trail { fill: none; }
.zkl-chapter-path { stroke: #d1ad6b9e; stroke-width: 1.25; }
.zkl-chapter-fill { stroke: var(--zkl-gold); stroke-width: 1.8; opacity: .9; }
.zkl-chapter-trail { stroke: var(--zkl-light); stroke-width: 2.6; stroke-linecap: round; filter: drop-shadow(0 0 5px #dda532); }
.zkl-chapter-head { fill: var(--zkl-light); filter: drop-shadow(0 0 4px #dda532); }
.zkl-chapter-stop {
  position: absolute; left: 14px; width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  transform: translate(-50%, -50%); pointer-events: auto;
  color: #c5b7a6; text-decoration: none; -webkit-tap-highlight-color: transparent;
  font-size: 12px; font-weight: 500;
}
.zkl-chapter-stop:nth-of-type(1) { top: 1.8%; }
.zkl-chapter-stop:nth-of-type(2) { top: 34%; }
.zkl-chapter-stop:nth-of-type(3) { top: 66%; }
.zkl-chapter-stop:nth-of-type(4) { top: 98.2%; }
.zkl-chapter-dot {
  box-sizing: border-box; width: 34px; height: 34px; border: 2px solid #c1a16c;
  border-radius: 50%; background: var(--zkl-rail-surface);
  display: grid; place-items: center; transition: background .2s, color .2s, border-color .2s;
}
.zkl-chapter-stop.is-past .zkl-chapter-dot { border-color: var(--zkl-gold); color: #f0ca79; }
.zkl-chapter-stop[aria-current="location"] .zkl-chapter-dot {
  background: var(--zkl-light); color: #2e2417; border-color: var(--zkl-light);
  box-shadow: 0 0 0 5px #dda53226, 0 0 18px #e4b44f66;
}
.zkl-chapter-stop:hover .zkl-chapter-dot { border-color: var(--zkl-light); }
.zkl-chapter-stop:focus-visible { outline: 2px solid var(--zkl-light); outline-offset: -1px; border-radius: 50%; }
.zkl-chapter-label {
  position: absolute; left: 52px; white-space: nowrap; direction: rtl;
  font-size: 12px; line-height: 1.3; font-weight: 400;
  color: #f4e5cb; background: #28221ef5; border: 1px solid #826642;
  padding: 7px 11px; border-radius: 999px;
  opacity: 1; transform: none; transition: opacity .2s, transform .2s;
  pointer-events: none;
}
.zkl-chapter-stop:hover .zkl-chapter-label,
.zkl-chapter-stop:focus-visible .zkl-chapter-label,
.zkl-chapter-stop[aria-current="location"] .zkl-chapter-label { opacity: 1; transform: none; }
@media (max-width: 767px) {
  .zkl-chapter-rail { --zkl-rail-width: 36px; --zkl-rail-top: 110px; --zkl-rail-bottom: 76px; left: 12px; width: 52px; }
  .zkl-chapter-dot { width: 30px; height: 30px; }
  .zkl-chapter-caption { font-size: 11px; }
  .zkl-chapter-label { opacity: 0; }
  .zkl-chapter-stop[aria-current="location"] .zkl-chapter-label { opacity: 0; }
  .zkl-chapter-stop:hover .zkl-chapter-label,
  .zkl-chapter-stop:focus-visible .zkl-chapter-label { opacity: 1; }
}
@media (max-height: 500px) {
  .zkl-chapter-rail { --zkl-rail-top: 80px; --zkl-rail-bottom: 35px; }
}
@media (prefers-reduced-motion: reduce) {
  .zkl-chapter-stop, .zkl-chapter-dot, .zkl-chapter-label { transition: none; }
  .zkl-chapter-trail { display: none; }
  .zkl-chapter-head { filter: none; }
}
JS
<script>
(() => {
  'use strict';
  // Safe to paste once in a global footer, or below the HTML widget.
  window.ZakaiChapterRail?.destroy();
  const cleanups = [];
  let destroyed = false;
  function init() {
    if (destroyed) return;
    document.querySelectorAll('.zkl-chapter-rail').forEach(rail => {
      if (rail.dataset.ready) return;
      const rootId = rail.getAttribute('data-scroll-container');
      const scroller = rootId ? document.getElementById(rootId) : null;
      if (rootId && !scroller) return;
      // Elementor demo wrappers may have backdrop-filter/transform/overflow.
      // A page-level fixed rail must live outside those containing blocks.
      if (!scroller && rail.parentElement !== document.body) {
        document.body.appendChild(rail);
      }
      const root = scroller || window;
      const path = rail.querySelector('.zkl-chapter-path');
      const fill = rail.querySelector('.zkl-chapter-fill');
      const trail = rail.querySelector('.zkl-chapter-trail');
      const head = rail.querySelector('.zkl-chapter-head');
      if (!path || !fill || !trail || !head) return;
      const allLinks = [...rail.querySelectorAll('.zkl-chapter-stop')];
      const items = allLinks.map(link => {
        const href = link.getAttribute('href') || '';
        let section = null;
        try { section = href.startsWith('#') ? document.getElementById(decodeURIComponent(href.slice(1))) : null; } catch (_) {}
        link.hidden = !section || !!(scroller && !scroller.contains(section));
        return {link, section, top: 0};
      }).filter(item => !item.link.hidden);
      if (!items.length) { rail.hidden = true; return; }
      rail.hidden = false;
      rail.dataset.ready = 'true';
      const motion = matchMedia('(prefers-reduced-motion: reduce)');
      let length = path.getTotalLength(), dirty = true, raf = 0, current = -1;
      let offset = 0, pending = null;
      const y = () => scroller ? scroller.scrollTop : (document.scrollingElement || document.documentElement).scrollTop;
      const maxY = () => Math.max(0, scroller ? scroller.scrollHeight - scroller.clientHeight : (document.scrollingElement || document.documentElement).scrollHeight - innerHeight);
      function measure() {
        offset = Math.max(0, parseFloat(getComputedStyle(rail).getPropertyValue('--zkl-scroll-offset')) || 0);
        const origin = scroller ? scroller.getBoundingClientRect().top + scroller.clientTop : 0;
        const scroll = y(), max = maxY();
        items.forEach(item => { item.top = Math.min(max, Math.max(0, item.section.getBoundingClientRect().top - origin + scroll - offset)); });
        // Links should follow the visual order of the chapters.
        items.sort((a, b) => a.top - b.top);
        length = path.getTotalLength();
        items.forEach((item, i) => {
          const point = path.getPointAtLength(items.length === 1 ? 0 : length * i / (items.length - 1));
          item.link.style.left = `${point.x / 28 * 100}%`;
          item.link.style.top = `${point.y / 1000 * 100}%`;
        });
        dirty = false;
      }
      function finishFocus(item) {
        const heading = item.section.querySelector('[data-zkl-chapter-heading], h1, h2, h3') || item.section;
        if (!heading.hasAttribute('tabindex')) heading.setAttribute('tabindex', '-1');
        heading.focus({preventScroll: true});
      }
      function render() {
        raf = 0;
        if (dirty) measure();
        const scroll = y();
        let index = 0;
        for (let i = 1; i < items.length; i++) if (scroll >= items[i].top - 2) index = i;
        const next = items[index + 1];
        const span = next ? next.top - items[index].top : 0;
        const t = next && span > 0 ? Math.max(0, Math.min(1, (scroll - items[index].top) / span)) : 0;
        const p = items.length > 1 ? (index + t) / (items.length - 1) : 0;
        const point = path.getPointAtLength(length * p);
        head.setAttribute('cx', point.x); head.setAttribute('cy', point.y);
        fill.style.strokeDasharray = `${length} ${length}`;
        fill.style.strokeDashoffset = String(length * (1 - p));
        const tail = Math.min(75, length * p);
        trail.style.strokeDasharray = `${tail} ${length}`;
        trail.style.strokeDashoffset = String(-Math.max(0, length * p - tail));
        rail.dataset.progress = p.toFixed(4);
        if (index !== current) {
          current = index;
          items.forEach((item, i) => {
            if (i === index) item.link.setAttribute('aria-current', 'location');
            else item.link.removeAttribute('aria-current');
            item.link.classList.toggle('is-past', i < index);
          });
          rail.dataset.chapter = String(index + 1);
          const name = items[index].link.querySelector('.zkl-chapter-label')?.textContent || items[index].section.id;
          rail.dispatchEvent(new CustomEvent('zkl:chapterchange', {bubbles: true, detail: {index, total: items.length, name, id: items[index].section.id}}));
        }
        if (pending && Math.abs(scroll - pending.top) < 3) {
          finishFocus(pending); pending = null;
        }
      }
      function schedule() { if (!raf) raf = requestAnimationFrame(render); }
      function resize() { dirty = true; schedule(); }
      function cancelPending() { pending = null; }
      const clicks = items.map(item => {
        function click(event) {
          if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
          event.preventDefault(); measure(); pending = item;
          root.scrollTo({top: item.top, behavior: motion.matches ? 'instant' : 'smooth'});
          if (!scroller) { try { history.replaceState(null, '', '#' + encodeURIComponent(item.section.id)); } catch (_) {} }
          schedule();
        }
        item.link.addEventListener('click', click);
        return () => item.link.removeEventListener('click', click);
      });
      root.addEventListener('scroll', schedule, {passive: true});
      root.addEventListener('wheel', cancelPending, {passive: true});
      root.addEventListener('touchstart', cancelPending, {passive: true});
      addEventListener('resize', resize);
      addEventListener('pageshow', resize);
      motion.addEventListener('change', resize);
      const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
      observer?.observe(scroller || document.body);
      items.forEach(item => observer?.observe(item.section));
      document.fonts?.ready.then(() => { if (!destroyed) resize(); });
      render();
      cleanups.push(() => {
        cancelAnimationFrame(raf); observer?.disconnect(); clicks.forEach(fn => fn());
        root.removeEventListener('scroll', schedule);
        root.removeEventListener('wheel', cancelPending);
        root.removeEventListener('touchstart', cancelPending);
        removeEventListener('resize', resize); removeEventListener('pageshow', resize);
        motion.removeEventListener('change', resize); delete rail.dataset.ready;
      });
    });
  }
  window.ZakaiChapterRail = {
    init,
    refresh() { dispatchEvent(new Event('resize')); },
    destroy() { destroyed = true; document.removeEventListener('DOMContentLoaded', init); cleanups.splice(0).forEach(fn => fn()); }
  };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, {once: true});
  else init();
})();

</script>
מה ניתן לשינוי
  • שמות ומספר הפרקים: ערכי, הוסיפי או הסירי קישורי zkl-chapter-stop ב־HTML. לכל תחנה צריך להיות קונטיינר בעל ID תואם. הסקריפט מסדר את התחנות לפי מיקום הקונטיינרים בפועל.
  • צבע האור והמסילה: המשתנים --zkl-gold, --zkl-light ו־--zkl-rail-surface.
  • רוחב המסילה: --zkl-rail-width, ברירת מחדל 56px במחשב ו־36px בנייד. שטח הלחיצה של התחנות הוא 44px.
  • מרווח עליון ותחתון למסלול: --zkl-rail-top ו־--zkl-rail-bottom. אלו מזיזים את התחנות בתוך המסילה.
  • מרווח בגלילה לפרק: --zkl-scroll-offset. אפשר להגדיר ערך שונה במחשב ובנייד.
  • מסילה ישרה: החליפי את d בשלושת המסלולים ב־M14 18 L14 982.
  • שם התחנה: כל שמות הפרקים גלויים במחשב. בנייד שמות התחנות זמינים בלחיצה ובניווט מקלדת בלי להשאיר תווית מעל התוכן.
  • תנועה מופחתת: מעבר ישיר לפרק, בלי גלילה חלקה ובלי שובל אור. סימון הפרק ממשיך לעבוד.
  • אחרי שינוי גובה התוכן: מתעדכן אוטומטית עם ResizeObserver. אם תוסף משנה את מיקום המקטעים בלי לשנות גובה, אפשר לקרוא ל־window.ZakaiChapterRail.refresh().

העמוד הוא דוגמה עצמאית. האתר החי לא נערך במסגרת ההכנה.

מתלבט? בוא/י לדבר איתי בווטסאפ?