→ לכל המאגר

B55 · מחוון שמחליק בין פריטי תפריט

התנהגויות GSAP ניווט ומבנהhover ומיקרו ממתין

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

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

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

הדמו

הבחירה הנוכחית: הכל

שלוש נקודות שמפרידות בין מימוש עובד למימוש שנשבר: המחוון מונפש גם ברוחב ולא רק במיקום, אחרת מילים באורך שונה נחתכות; המדידה חוזרת אחרי `document.fonts.ready`, כי עד שהפונט נטען רוחב המילים שונה והמחוון מתחיל במקום הלא נכון; ויציאה מהתפריט מחזירה אותו לפריט הפעיל במקום להשאיר אותו על האחרון שרוחפים מעליו. `offsetLeft` ו-`offsetTop` הם ערכים פיזיים, ולכן אותו קוד עובד בעברית ובאנגלית בלי תנאים. **ומלכודת ניגודיות**: הצבע הלבן חייב להיקבע לפי מי שהגלולה נמצאת מתחתיו ברגע זה ולא לפי הפריט הנבחר, אחרת בהובר מקבלים טקסט אפור על גלולה כהה ובמקביל טקסט לבן על רקע בהיר. הבחירה עצמה מסומנת ב-aria-current.

קוד להדבקה

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

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

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}

/* B55 · מחוון שמחליק בין פריטי תפריט */
.tg{display:inline-flex;position:relative;background:var(--card, #fff);border:1px solid var(--line, #e4e4ee);
  border-radius:999px;padding:5px;gap:2px;max-width:100%;flex-wrap:wrap}
.tg-pill{position:absolute;top:5px;left:0;height:calc(100% - 10px);border-radius:999px;background:var(--ink, #16182b);z-index:0}
.tg a{position:relative;z-index:1;padding:10px 20px;border-radius:999px;font-size:15px;color:var(--muted, #6a6d85);
  text-decoration:none;white-space:nowrap;transition:color .22s}
/* הצבע הלבן נקבע לפי מי שהגלולה נמצאת מתחתיו ברגע זה (hot), ולא לפי מי שנבחר (on).
   אחרת בהובר הטקסט האפור יושב על גלולה כהה, והנבחר נשאר לבן על רקע בהיר. */
.tg a.hot{color:var(--bg, #f7f7fa)}
.tg a:hover{color:var(--ink, #16182b)}
.tg a.hot:hover{color:var(--bg, #f7f7fa)}
.tg a:focus-visible{outline:2px solid var(--accent, #4a3aff);outline-offset:2px}
.tg-wrap{display:grid;justify-items:center;gap:26px}
.tg-out{color:var(--muted, #6a6d85);font-size:15px}
.tg-out b{color:var(--ink, #16182b)}
@media(max-width:520px){.tg a{padding:10px 13px;font-size:14px}}

HTML

<div class="stage tight"><div class="tg-wrap">
  <nav class="tg">
    <i class="tg-pill"></i>
    <a href="#" class="on" aria-current="true">הכל</a><a href="#">אתרי תדמית</a><a href="#">דפי נחיתה</a><a href="#">חנויות</a><a href="#">מערכות</a>
  </nav>
  <p class="tg-out">הבחירה הנוכחית: <b class="tg-now">הכל</b></p>
</div></div>

JS

(function(){
  const nav=document.querySelector(".tg"),pill=nav.querySelector(".tg-pill"),out=document.querySelector(".tg-now");
  const links=[...nav.querySelectorAll("a")];
  const reduce=matchMedia("(prefers-reduced-motion: reduce)").matches;
  let active=links[0];
  // offsetLeft/offsetTop פיזיים, ולכן החישוב זהה בעברית ובאנגלית
  const move=(el,animate)=>{
    links.forEach(x=>x.classList.toggle("hot",x===el));
    const to={x:el.offsetLeft,y:el.offsetTop-5,width:el.offsetWidth,height:el.offsetHeight};
    if(animate&&!reduce)gsap.to(pill,{...to,duration:.4,ease:"back.out(1.4)"});
    else gsap.set(pill,to);
  };
  links.forEach(a=>{
    a.addEventListener("mouseenter",()=>move(a,true));
    a.addEventListener("focus",()=>move(a,true));
    a.addEventListener("click",e=>{
      e.preventDefault();
      active=a;links.forEach(x=>{x.classList.toggle("on",x===a);
        x===a?x.setAttribute("aria-current","true"):x.removeAttribute("aria-current")});
      out.textContent=a.textContent;move(a,true);
    });
  });
  // יציאה מהתפריט מחזירה את המחוון לפריט שנבחר, ולא משאירה אותו במקום אקראי
  nav.addEventListener("mouseleave",()=>move(active,true));
  nav.addEventListener("focusout",e=>{if(!nav.contains(e.relatedTarget))move(active,true);});
  move(active,false);
  addEventListener("resize",()=>move(active,false));
  document.fonts&&document.fonts.ready.then(()=>move(active,false));  // הפונט משנה רוחב מילים
})();