→ לכל המאגר

B50 · מתג מצב כהה עם מעבר וזיכרון

התנהגויות CSS vars · GSAP ניווט ומבנהרקע ואווירהטעינה ופידבק ממתין

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

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

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

הדמו

מצב תצוגה

קריאה ארוכה

מאמרים ומדריכים נוחים יותר לעין בלילה.

ממשק ניהול

מסכים שיושבים מולם שעות דורשים בחירה.

תיק עבודות

רקע כהה מבליט צילומים וויז`ואל.

הערכה כולה יושבת במשתני CSS על מעטפת אחת ולא על :root, כדי שאפשר יהיה להטמיע את הרכיב בעמוד שכבר יש לו ערכה משלו. שלושה דברים שהופכים את זה למקצועי: ברירת המחדל מגיעה מ-prefers-color-scheme ורק אז נדרסת בבחירה שמורה, ההחלפה קורית בזמן שהגל מכסה את המסך (לפני כן רואים הבזק), וכל גישה ל-localStorage עטופה ב-try כי בגלישה פרטית היא זורקת. במצב חיסכון בתנועה ההחלפה מיידית בלי גל.

קוד להדבקה

הקוד כאן עומד בפני עצמו. כל טוקן עיצוב נושא ברירת מחדל, מחלקות העזר של המאגר מוטמעות בו, ותגי הסקריפט מסודרים לפי הסדר הנכון. פרויקט שמגדיר --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}

/* B50 · מתג מצב כהה עם מעבר וזיכרון */
.tt{position:relative;overflow:hidden;border-radius:var(--r, 14px);border:1px solid var(--line, #e4e4ee);
  --tt-bg:#ffffff;--tt-ink:#16182b /* qa-allow: literal, הדמו מגדיר שני ערכות משלו */;--tt-mut:#5b5f77;--tt-card:#f4f4f8;--tt-line:#e3e3ec;
  background:var(--tt-bg);color:var(--tt-ink);padding:clamp(24px,4vw,54px);
  transition:background-color .35s ease,color .35s ease}
.tt[data-theme="dark"]{--tt-bg:#111219;--tt-ink:#f2f2f7;--tt-mut:#a2a5bb;--tt-card:#1b1d27;--tt-line:#2c2f3d}
.tt-top{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:24px}
.tt-top h3{margin:0;font-size:clamp(20px,2.4vw,32px)}
.tt-btn{position:relative;z-index:2;display:inline-flex;align-items:center;gap:9px;border:1px solid var(--tt-line);
  background:var(--tt-card);color:var(--tt-ink);font:inherit;font-size:14px;padding:10px 16px;border-radius:999px;cursor:pointer}
.tt-ico{font-size:15px;line-height:1}
.tt-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
.tt-card{background:var(--tt-card);border:1px solid var(--tt-line);border-radius:14px;padding:18px;
  transition:background-color .35s ease,border-color .35s ease}
.tt-card h4{margin:0 0 6px;font-size:16px}
.tt-card p{margin:0;font-size:14px;line-height:1.6;color:var(--tt-mut)}
/* הגל: עיגול שנפתח מהכפתור, ומתחתיו מחליפים את הערכה */
.tt-wave{position:absolute;border-radius:50%;pointer-events:none;z-index:5;transform:translate(-50%,-50%) scale(0)}
@media(max-width:700px){.tt-grid{grid-template-columns:1fr}}
@media (prefers-reduced-motion: reduce){.tt,.tt-card{transition:none}}

HTML

<div class="stage tight"><div class="tt">
  <div class="tt-top">
    <h3>מצב תצוגה</h3>
    <button class="tt-btn" aria-pressed="false"><span class="tt-ico">🌙</span><span class="tt-lbl">מצב כהה</span></button>
  </div>
  <div class="tt-grid">
    <div class="tt-card"><h4>קריאה ארוכה</h4><p>מאמרים ומדריכים נוחים יותר לעין בלילה.</p></div>
    <div class="tt-card"><h4>ממשק ניהול</h4><p>מסכים שיושבים מולם שעות דורשים בחירה.</p></div>
    <div class="tt-card"><h4>תיק עבודות</h4><p>רקע כהה מבליט צילומים וויז`ואל.</p></div>
  </div>
</div></div>

JS

(function(){
  const root=document.querySelector(".tt"),btn=root.querySelector(".tt-btn");
  const ico=btn.querySelector(".tt-ico"),lbl=btn.querySelector(".tt-lbl");
  const reduce=matchMedia("(prefers-reduced-motion: reduce)").matches;
  const KEY="mv-tt-theme";
  let saved=null;try{saved=localStorage.getItem(KEY);}catch(e){}
  // ברירת המחדל היא ההגדרה של המכשיר, והבחירה של המשתמש גוברת עליה
  let dark=saved?saved==="dark":matchMedia("(prefers-color-scheme: dark)").matches;
  function paint(){
    root.dataset.theme=dark?"dark":"light";
    btn.setAttribute("aria-pressed",String(dark));
    ico.textContent=dark?"☀️":"🌙";
    lbl.textContent=dark?"מצב בהיר":"מצב כהה";
    try{localStorage.setItem(KEY,dark?"dark":"light");}catch(e){}
  }
  paint();
  btn.addEventListener("click",()=>{
    if(reduce){dark=!dark;paint();return;}
    const b=btn.getBoundingClientRect(),r=root.getBoundingClientRect();
    const cx=b.left+b.width/2-r.left,cy=b.top+b.height/2-r.top;
    // רדיוס שמכסה בוודאות את הפינה הרחוקה ביותר
    const rad=Math.hypot(Math.max(cx,r.width-cx),Math.max(cy,r.height-cy));
    const wave=document.createElement("span");
    wave.className="tt-wave";
    wave.style.cssText="left:"+cx+"px;top:"+cy+"px;width:"+rad*2+"px;height:"+rad*2+"px;background:"+
      (dark?"#ffffff":"#111219");
    root.appendChild(wave);
    gsap.timeline({onComplete:()=>wave.remove()})
      .to(wave,{scale:1,duration:.45,ease:"power2.out"})
      .add(()=>{dark=!dark;paint();})              // מחליפים ערכה מתחת לגל, לא לפניו
      .to(wave,{opacity:0,duration:.35,ease:"power1.out"},"+=0.05");
  });
})();