→ לכל המאגר

B32 · טופס עם תוויות צפות, שגיאות ומצב שליחה

התנהגויות CSS :placeholder-shown · JS טעינה ופידבקטקסט וכותרות ממתין

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

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

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

הדמו

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

הדגמה בלבד. שום דבר לא נשלח לשום מקום.

התווית הצפה עובדת בלי JS בזכות :placeholder-shown, ולכן חובה placeholder של רווח בודד בכל שדה. השגיאה מנוקה תוך כדי הקלדה ולא רק בשליחה הבאה, וזה ההבדל בין טופס שמעצבן לטופס שעוזר. הכפתור מחזיק שלושה מצבים באותו רוחב כדי שלא יקפוץ, ובפרודקשן מוסיפים גם aria-live להודעת ההצלחה.

קוד להדבקה

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

/* B32 · טופס עם תוויות צפות, שגיאות ומצב שליחה */
.ff{max-width:min(560px,92vw);margin-inline:auto;display:grid;gap:18px}
.ff-field{position:relative}
.ff-field input,.ff-field textarea{width:100%;font:inherit;font-size:16px;color:var(--ink, #16182b);background:var(--card, #fff);
  border:1px solid var(--line, #e4e4ee);border-radius:12px;padding:26px 16px 10px;transition:border-color .25s,box-shadow .25s}
.ff-field textarea{min-height:120px;resize:vertical}
.ff-field input:focus,.ff-field textarea:focus{outline:0;border-color:var(--accent, #4a3aff);box-shadow:0 0 0 3px rgba(74,58,255,.13)}
.ff-field label{position:absolute;inset-inline-start:17px;top:17px;color:var(--muted, #6a6d85);font-size:16px;pointer-events:none;
  transform-origin:0 0;transition:transform .22s cubic-bezier(.2,.6,.2,1),color .22s}
/* התווית עולה כשהשדה בפוקוס או כשיש בו תוכן. placeholder ריק הוא התנאי לזיהוי */
.ff-field input:focus+label,.ff-field textarea:focus+label,
.ff-field input:not(:placeholder-shown)+label,.ff-field textarea:not(:placeholder-shown)+label{transform:translateY(-11px) scale(.78)}
.ff-field input:focus+label,.ff-field textarea:focus+label{color:var(--accent, #4a3aff)}
.ff-err{display:block;font-size:13px;color:#c2255c;margin-top:6px;height:0;opacity:0;transition:opacity .25s}
.ff-field.bad input,.ff-field.bad textarea{border-color:#c2255c;animation:ffshake .34s}
.ff-field.bad .ff-err{height:auto;opacity:1}
@keyframes ffshake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
.ff-btn{position:relative;min-height:54px;border:0;border-radius:999px;background:var(--accent, #4a3aff);color:var(--accent-ink, #fff);font:inherit;font-weight:600;
  font-size:17px;cursor:pointer;overflow:hidden;transition:background .3s}
.ff-btn span{display:block;transition:transform .3s cubic-bezier(.2,.6,.2,1),opacity .25s}
.ff-btn .s2,.ff-btn .s3{position:absolute;inset:0;display:grid;place-items:center;transform:translateY(110%);opacity:0}
.ff-btn.load .s1{transform:translateY(-110%);opacity:0}
.ff-btn.load .s2{transform:none;opacity:1}
.ff-btn.done{background:#2b8a3e}
.ff-btn.done .s1,.ff-btn.done .s2{transform:translateY(-110%);opacity:0}
.ff-btn.done .s3{transform:none;opacity:1}
.ff-spin{width:17px;height:17px;border-radius:50%;border:2px solid rgba(255,255,255,.4);border-top-color:#fff;animation:ffspin .7s linear infinite;display:inline-block}
@keyframes ffspin{to{transform:rotate(360deg)}}
.ff-note{font-size:13px;color:var(--muted, #6a6d85);text-align:center}
@media (prefers-reduced-motion: reduce){.ff-field.bad input{animation:none}.ff-spin{animation-duration:2s}}

HTML

<div class="stage tight"><form class="ff" novalidate>
  <div class="ff-field"><input type="text" id="ffn" placeholder=" " autocomplete="name"><label for="ffn">שם מלא</label><small class="ff-err">צריך שם כדי לדעת למי לחזור</small></div>
  <div class="ff-field"><input type="email" id="ffe" placeholder=" " autocomplete="email"><label for="ffe">אימייל</label><small class="ff-err">כתובת האימייל לא נראית תקינה</small></div>
  <div class="ff-field"><textarea id="ffm" placeholder=" "></textarea><label for="ffm">במה נוכל לעזור?</label><small class="ff-err">כמה מילים יעזרו לנו להתכונן</small></div>
  <button class="ff-btn" type="submit"><span class="s1">שליחה</span><span class="s2"><i class="ff-spin"></i></span><span class="s3">נשלח, נחזור אליך</span></button>
  <p class="ff-note">הדגמה בלבד. שום דבר לא נשלח לשום מקום.</p>
</form></div>

JS

(function(){
  const form=document.querySelector(".ff"),btn=form.querySelector(".ff-btn");
  const fields=[...form.querySelectorAll(".ff-field")];
  function bad(f){
    const el=f.querySelector("input,textarea");
    const v=el.value.trim();
    if(el.type==="email")return !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v);
    return v.length<2;
  }
  fields.forEach(f=>{
    const el=f.querySelector("input,textarea");
    // מסירים את השגיאה ברגע שמתקנים, לא רק בשליחה הבאה
    el.addEventListener("input",()=>{if(f.classList.contains("bad")&&!bad(f))f.classList.remove("bad");});
    el.addEventListener("blur",()=>{if(el.value.trim())f.classList.toggle("bad",bad(f));});
  });
  form.addEventListener("submit",e=>{
    e.preventDefault();
    let first=null;
    fields.forEach(f=>{const b=bad(f);f.classList.toggle("bad",b);if(b&&!first)first=f;});
    if(first){first.querySelector("input,textarea").focus();return;}
    btn.classList.add("load");
    setTimeout(()=>{btn.classList.remove("load");btn.classList.add("done");},1400);
  });
})();