→ לכל המאגר

B34 · גריד מסונן עם מעבר חלק

התנהגויות GSAP · Flip כרטיסיםתמונות וגלריותניווט ומבנה ממתין

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

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

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

הדמו

1

משרד עורכי דין

אתר תדמית

2

חנות תכשיטים

חנות

3

קמפיין קיץ

דף נחיתה

4

קליניקת שיניים

אתר תדמית

5

חנות רהיטים

חנות

6

וובינר

דף נחיתה

7

חברת בנייה

אתר תדמית

8

חנות קפה

חנות

9

קורס דיגיטלי

דף נחיתה

אין פריטים בקטגוריה הזאת.

Flip פותר את הבעיה האמיתית בגריד מסונן: בלעדיו הפריטים קופצים למקום החדש בפריים אחד. absolute:true מונע קפיצה כשמספר השורות משתנה, אבל הוא גם מרוקן את הגריד ולכן חייבים להנפיש את גובהו במקביל, ו-onEnter ו-onLeave מטפלים בפריטים שנכנסים ויוצאים לגמרי. תחת prefers-reduced-motion הסינון עדיין עובד, פשוט בלי תנועה.

קוד להדבקה

הקוד כאן עומד בפני עצמו. כל טוקן עיצוב נושא ברירת מחדל, מחלקות העזר של המאגר מוטמעות בו, ותגי הסקריפט מסודרים לפי הסדר הנכון. פרויקט שמגדיר --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}
.ph{display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;border-radius:var(--r, 14px)}
.ph-a{background:linear-gradient(160deg,#3b5bdb,#748ffc)}
.ph-b{background:linear-gradient(160deg,#5f3dc4,#9775fa)}
.ph-c{background:linear-gradient(160deg,#0b7285,#3bc9db)}
.ph-d{background:linear-gradient(160deg,#2b8a3e,#69db7c)}
.ph-e{background:linear-gradient(160deg,#e8590c,#ffa94d)}
.ph-f{background:linear-gradient(160deg,#c92a2a,#ff8787)}
.ph-l{background:rgba(16,18,43,.72);color:#fff;padding:.18em .7em;border-radius:999px;line-height:1.3;font-size:max(.78em,12px)}

/* B34 · גריד מסונן עם מעבר חלק */
.fg{padding-inline:var(--gutter, clamp(24px, 5vw, 96px))}
.fg-bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:clamp(20px,2.4vw,34px)}
.fg-chip{border:1px solid var(--line, #e4e4ee);background:var(--card, #fff);border-radius:999px;padding:10px 18px;font:inherit;font-size:14px;
  color:var(--ink, #16182b);cursor:pointer;transition:background .25s,border-color .25s,color .25s}
.fg-chip:hover{border-color:var(--accent, #4a3aff)}
.fg-chip.on{background:var(--ink, #16182b);color:var(--bg, #f7f7fa);border-color:var(--ink, #16182b)}
.fg-count{margin-inline-start:auto;font-size:13px;color:var(--muted, #6a6d85);font-variant-numeric:tabular-nums}
.fg-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--gap, clamp(16px, 1.6vw, 36px));position:relative;background:var(--bg, #f7f7fa)}
.fg-item{background:var(--card, #fff);border:1px solid var(--line, #e4e4ee);border-radius:var(--r, 14px);overflow:hidden}
.fg-item .ph{height:clamp(150px,15vw,210px);border-radius:0;font-size:22px}
.fg-item h3{margin:0;padding:16px 18px 4px;font-size:17px}
.fg-item p{margin:0;padding:0 18px 18px;font-size:13px;color:var(--muted, #6a6d85)}
.fg-item.hide{display:none}
.fg-empty{padding:40px 4px;color:var(--muted, #6a6d85);text-align:center;display:none}
.fg-empty.on{display:block}
@media(max-width:900px){.fg-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.fg-grid{grid-template-columns:1fr}}

HTML

<div class="stage tight"><div class="fg">
  <div class="fg-bar" role="group" aria-label="סינון לפי קטגוריה">
    <button class="fg-chip on" data-f="all">הכל</button>
    <button class="fg-chip" data-f="web">אתרי תדמית</button>
    <button class="fg-chip" data-f="shop">חנויות</button>
    <button class="fg-chip" data-f="lp">דפי נחיתה</button>
    <span class="fg-count"></span>
  </div>
  <div class="fg-grid">
    <article class="fg-item" data-c="web"><div class="ph ph-a"><span class="ph-l">1</span></div><h3>משרד עורכי דין</h3><p>אתר תדמית</p></article>
    <article class="fg-item" data-c="shop"><div class="ph ph-b"><span class="ph-l">2</span></div><h3>חנות תכשיטים</h3><p>חנות</p></article>
    <article class="fg-item" data-c="lp"><div class="ph ph-c"><span class="ph-l">3</span></div><h3>קמפיין קיץ</h3><p>דף נחיתה</p></article>
    <article class="fg-item" data-c="web"><div class="ph ph-d"><span class="ph-l">4</span></div><h3>קליניקת שיניים</h3><p>אתר תדמית</p></article>
    <article class="fg-item" data-c="shop"><div class="ph ph-e"><span class="ph-l">5</span></div><h3>חנות רהיטים</h3><p>חנות</p></article>
    <article class="fg-item" data-c="lp"><div class="ph ph-f"><span class="ph-l">6</span></div><h3>וובינר</h3><p>דף נחיתה</p></article>
    <article class="fg-item" data-c="web"><div class="ph ph-c"><span class="ph-l">7</span></div><h3>חברת בנייה</h3><p>אתר תדמית</p></article>
    <article class="fg-item" data-c="shop"><div class="ph ph-a"><span class="ph-l">8</span></div><h3>חנות קפה</h3><p>חנות</p></article>
    <article class="fg-item" data-c="lp"><div class="ph ph-b"><span class="ph-l">9</span></div><h3>קורס דיגיטלי</h3><p>דף נחיתה</p></article>
  </div>
  <p class="fg-empty">אין פריטים בקטגוריה הזאת.</p>
</div></div>

JS

gsap.registerPlugin(Flip);

(function(){
  const chips=[...document.querySelectorAll(".fg-chip")],items=[...document.querySelectorAll(".fg-item")];
  const count=document.querySelector(".fg-count"),empty=document.querySelector(".fg-empty");
  const grid=document.querySelector(".fg-grid");
  const reduce=matchMedia("(prefers-reduced-motion: reduce)").matches;
  function apply(f,animate){
    // Flip: מצלמים את המצב לפני, משנים את ה-DOM, והספרייה מנפישה את ההפרש
    const state=Flip.getState(items,{props:"opacity"});
    const h0=grid.getBoundingClientRect().height;
    let shown=0;
    items.forEach(it=>{
      const vis=f==="all"||it.dataset.c===f;
      it.classList.toggle("hide",!vis);
      if(vis)shown++;
    });
    count.textContent=shown+" מתוך "+items.length;
    empty.classList.toggle("on",shown===0);
    if(reduce||!animate)return;                 // הקריאה הראשונה רק מסמנת מצב, בלי אנימציה על טעינה
    const h1=grid.getBoundingClientRect().height;
    // absolute:true מוציא את הכרטיסים מהזרימה, הגריד מתכווץ לאפס, וכל מה שמתחתיו
    // עולה למעלה ומציץ מאחורי הכרטיסים המרחפים. מנפישים את גובה הגריד במקביל.
    gsap.fromTo(grid,{height:h0},{height:h1,duration:.55,ease:"power2.inOut",
      onComplete:()=>gsap.set(grid,{clearProps:"height"})});
    Flip.from(state,{duration:.55,ease:"power2.inOut",scale:true,absolute:true,
      onEnter:els=>gsap.fromTo(els,{opacity:0,scale:.9},{opacity:1,scale:1,duration:.4}),
      onLeave:els=>gsap.to(els,{opacity:0,scale:.9,duration:.25})});
  }
  chips.forEach(c=>c.addEventListener("click",()=>{
    chips.forEach(x=>x.classList.toggle("on",x===c));
    apply(c.dataset.f,true);
  }));
  apply("all",false);
})();