ב-11 פרקים למדתם לבנות אנימציות — מ-CSS transitions דרך GSAP ו-ScrollTrigger ועד Canvas ו-3D. אבל חשבו על האתרים שאתם באמת אוהבים להשתמש בהם. מה גורם להם להרגיש "מלוטשים"? זה לא ה-hero section המרשים. זה לא ה-scroll animation. זה הפרטים הקטנים: הכפתור שנותן feedback מיידי כשלוחצים עליו. ה-toggle שמחליק בצורה מספקת. ה-validation שמראה V ירוק ברגע שהסיסמה תקינה. הקליק הקטן שנשמע כשמפעילים dark mode. אלה מיקרו-אינטראקציות — אנימציות קטנות שמתקשרות state, נותנות feedback, ויוצרות תחושה של "מישהו חשב על הפרט הזה". ובפרק הזה תלמדו לבנות אותן. תלמדו גם להוסיף צלילים — שכבה שרוב המפתחים מתעלמים ממנה לחלוטין, אבל שמשנה את החוויה בצורה דרמטית. מ-button ripples דרך form validation animations ועד Web Audio API — הכל בפרק הזה.
בפרק 11 בניתם סצנות 3D עם Three.js, Spline ו-React Three Fiber. עכשיו אתם חוזרים לפרטים הקטנים — המיקרו-אינטראקציות שעושות את ההבדל בין "אתר שעובד" ל-"אתר שמרגיש מושלם". בפרק הזה תבנו ספריית מיקרו-אינטראקציות לפרויקט שלכם: button feedback, form validation animations, toggle switches, hover effects, וצלילי UI. בפרק 13 תלמדו לתזמר את כל האנימציות יחד — מה-hero section דרך ה-scroll effects ועד המיקרו-אינטראקציות — לחוויה שלמה ומתואמת.
| מונח (English) | תרגום | הגדרה |
|---|---|---|
| Micro-interaction | מיקרו-אינטראקציה | אנימציה קטנה שמתקשרת state, נותנת feedback, או יוצרת תחושת delight. לחיצה על כפתור, toggle switch, validation indicator — כל אלה מיקרו-אינטראקציות. מורכבת מ-trigger, rules, feedback, ו-loops/modes |
| Feedback Loop | לולאת משוב | מעגל שבו פעולת המשתמש מקבלת תגובה ויזואלית/קולית מיידית, שמאשרת שהפעולה התקבלה. למשל: לחיצה → ripple → שינוי צבע → V. בלי feedback loop — המשתמש לא בטוח שמשהו קרה |
| Ripple Effect | אפקט אדווה | גל עיגולי שמתפשט מנקודת הלחיצה — הדפוס הקלאסי של Material Design. מראה למשתמש בדיוק איפה הוא לחץ ומאשר שהלחיצה התקבלה |
| AudioContext | הקשר אודיו | האובייקט המרכזי של Web Audio API — מייצג את כל מערכת השמע בדפדפן. חובה ליצור אחד לפני שמנגנים צלילים. חלק מהדפדפנים חוסמים אותו עד שהמשתמש עושה אינטראקציה (user gesture requirement) |
| Howler.js | האולר | ספריית JavaScript פופולרית לניגון צלילים באינטרנט. עוטפת את Web Audio API ו-HTML5 Audio, מטפלת ב-cross-browser issues, ומספקת API פשוט: load, play, pause, volume, sprite |
| Haptic Feedback | משוב הפטי / רטט | משוב פיזי (רטט) במכשירים ניידים. ב-web אפשר להפעיל דרך Vibration API: navigator.vibrate(50). שכבה נוספת של feedback מעבר לוויזואלי וקולי |
| State Animation | אנימציית מצב | אנימציה שמתרחשת כשרכיב UI עובר ממצב אחד לאחר: idle → hover → active → loading → success/error. כל מעבר מצב הוא הזדמנות למיקרו-אינטראקציה |
| Toggle | מתג / טוגל | רכיב UI שמחליף בין שני מצבים: on/off, light/dark, open/closed. האנימציה של ה-toggle היא אחת המיקרו-אינטראקציות הנפוצות ביותר באתרים |
| Skeleton Screen | מסך שלד | תצוגת placeholder שמראה את מבנה התוכן לפני שהוא נטען — צורות אפורות שמהבהבות (shimmer). חלופה מודרנית ל-spinner שמרגישה מהירה יותר |
| Magnetic Button | כפתור מגנטי | כפתור שנמשך לכיוון הסמן כשהוא מתקרב — כאילו יש מגנט. אפקט hover מתקדם שגורם ללחיצה להרגיש inevitably. נפוץ באתרי creative agencies |
| Cursor Follower | עוקב סמן | אלמנט מותאם אישית שעוקב אחרי הסמן עם עיכוב קל (lerp/spring). מחליף את הסמן הרגיל ומשתנה לפי ההקשר: גדל על כפתורים, מתכווץ על טקסט, משנה צבע על תמונות |
| Ambient Sound | צליל סביבתי | צליל רקע עדין שיוצר אווירה — white noise, צלילי טבע, מוזיקה שקטה. דורש שליטת volume מדויקת (0.05-0.15) ו-mute toggle נגיש. שימוש זהיר — רוב המשתמשים לא מצפים לצלילים באתרים |
| Tone.js | טון | ספריית Web Audio מתקדמת ליצירת מוזיקה וצלילים סינתטיים בדפדפן. מספקת synthesizers, effects, ותזמון מדויק. מתאימה לצלילי UI ייחודיים שנוצרים בקוד במקום מקבצי אודיו |
Dan Saffer, שכתב את הספר "Microinteractions" ב-2013, הגדיר מיקרו-אינטראקציה כ-"רגע אחד של שימוש שסובב סביב use case אחד". בואו נפרק את זה: כשאתם מחליקים toggle מ-off ל-on, זו מיקרו-אינטראקציה. כשאתם מקלידים סיסמה ומופיע פס ירוק שאומר "חזקה", זו מיקרו-אינטראקציה. כשאתם מושכים את הדף למטה ומופיע pull-to-refresh spinner, זו מיקרו-אינטראקציה. כשאתם "לייקים" פוסט ולב קופץ לרגע, זו מיקרו-אינטראקציה.
כל מיקרו-אינטראקציה מורכבת מארבעה חלקים:
למה מיקרו-אינטראקציות כל כך חשובות? שלוש סיבות:
1. Feedback — תקשורת מצב. בלי מיקרו-אינטראקציות, הממשק "שקט". אתם לוחצים על כפתור ולא קורה כלום — לפחות ויזואלית. אולי הבקשה נשלחה, אולי לא. אולי צריך ללחוץ שוב, אולי לא. Feedback מיידי — גם אם זה רק שינוי צבע של 200 מילישניות — אומר למשתמש: "שמעתי אותך, אני מטפל בזה". מחקרי UX מראים שתגובה ויזואלית תוך 100ms מרגישה "מיידית". בין 100ms ל-1000ms המשתמש מבחין בעיכוב אבל נשאר מחובר. מעל 1000ms — צריכים feedback מפורש (spinner, progress bar, הודעה).
2. State Communication — תקשורת מצבים. כל רכיב UI יכול להיות במצבים שונים: idle, hover, active, focused, loading, success, error, disabled. מיקרו-אינטראקציות מתקשרות את המעבר בין המצבים. כפתור שעובר מ-idle ל-loading צריך להראות שמשהו קורה (spinner, שינוי טקסט). שדה טופס שעובר מ-idle ל-error צריך להראות שיש בעיה (גבול אדום, הודעת שגיאה, shake). בלי אנימציות מעבר, המצב משתנה "בקפיצה" — וזה מבלבל.
3. Delight — הנאה. מעבר ל-functionality, מיקרו-אינטראקציות יכולות ליצור רגעי הנאה קטנים. ה-like animation של Twitter (עכשיו X) עם הזיקוקים הקטנים. ה-pull-to-refresh של Gmail עם הצבעים. ה-confetti של Linear כשמסיימים task. אלה לא הכרחיים ל-function, אבל הם מה שגורם למשתמשים לזכור את המוצר. הזהירות: delight בלי function מרגיש כמו גימיק. תמיד function first, delight second.
| שאלה | כן = להוסיף | לא = לוותר |
|---|---|---|
| האם המשתמש צריך feedback על פעולה? | לחיצה, שליחת טופס, שמירה, מחיקה | גלילה רגילה, קריאת טקסט |
| האם יש מעבר מצב? | idle→loading→success, open→closed | תוכן סטטי שלא משתנה |
| האם המשתמש עלול להיות מבולבל? | "הכפתור עבד?" / "הנתונים נשמרו?" | פעולות מובנות ממילא (קישור) |
| האם יש הזדמנות ל-delight בלי להפריע? | סיום task, achievement, Easter egg | אמצע flow קריטי (checkout) |
| האם האנימציה קצרה (<500ms)? | 100-300ms לרוב הפעולות | אנימציה ארוכה חוסמת interaction |
אנימציה דקורטיבית היא אנימציה שנראית יפה אבל לא מתקשרת מידע ולא נותנת feedback. רקע עם כוכבים שנעים, parallax על תמונות, floating elements — יפים, אבל לא מיקרו-אינטראקציות. ההבדל: מיקרו-אינטראקציה מגיבה לפעולת משתמש או מתקשרת שינוי מצב. אם זה רק "אנימציה שרצה ברקע" — זו אנימציה דקורטיבית. שניהם לגיטימיים, אבל הפרק הזה מתמקד באינטראקטיביות.
כפתורים הם הרכיב הכי אינטראקטיבי באתר. כל כפתור עובר דרך שרשרת מצבים: idle → hover → active (press) → loading → success/error → idle. כל מעבר הוא הזדמנות למיקרו-אינטראקציה. בואו נבנה את כולם.
שלב 1: Hover + Active states בסיסיים (CSS בלבד)
הבסיס של כל כפתור — hover שמראה שהכפתור clickable, ו-active שמראה שהלחיצה התקבלה:
/* כפתור בסיסי עם hover ו-active */
.btn {
padding: 12px 24px;
background: #6366f1;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
/* Hover — קצת בהיר יותר, צל קל, הרמה קלה */
.btn:hover {
background: #818cf8;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
transform: translateY(-1px);
}
/* Active — לחיצה: קטן קצת, צל קטן יותר */
.btn:active {
transform: translateY(0) scale(0.97);
box-shadow: 0 2px 4px rgba(99, 102, 241, 0.3);
transition-duration: 0.1s;
}
/* Focus — מקלדת: ring ברור לנגישות */
.btn:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
שימו לב לדקויות: hover מרים את הכפתור 1px למעלה (translateY(-1px)) ומוסיף צל — נותן תחושה של "ריחוף". active מחזיר אותו למקום ומקטין אותו ב-3% — תחושת "לחיצה פיזית". ה-transition-duration ב-active קצר יותר (0.1s) כי feedback על לחיצה צריך להיות מיידי. ו-focus-visible מבטיח ring רק מקלדת, לא mouse click.
שלב 2: Ripple Effect (Material Design style)
ה-ripple effect של Material Design הוא אחד מדפוסי המיקרו-אינטראקציה הכי מוכרים. גל עיגולי שמתפשט מנקודת הלחיצה — מראה בדיוק איפה לחצתם:
/* CSS לאפקט Ripple */
.btn-ripple {
position: relative;
overflow: hidden;
}
.btn-ripple .ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
transform: scale(0);
animation: ripple-expand 0.6s ease-out forwards;
pointer-events: none;
}
@keyframes ripple-expand {
to {
transform: scale(4);
opacity: 0;
}
}
// JavaScript — יוצר ripple בנקודת הלחיצה
document.querySelectorAll('.btn-ripple').forEach(button => {
button.addEventListener('click', function(e) {
// מחשב את מיקום הלחיצה ביחס לכפתור
const rect = this.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// יוצר את אלמנט ה-ripple
const ripple = document.createElement('span');
ripple.classList.add('ripple');
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
ripple.style.width = ripple.style.height =
Math.max(rect.width, rect.height) + 'px';
// מוסיף ומסיר אחרי האנימציה
this.appendChild(ripple);
ripple.addEventListener('animationend', () => ripple.remove());
});
});
שלב 3: Loading State — מכפתור ל-spinner
אחרי לחיצה על כפתור שמבצע פעולה אסינכרונית (submit, save, send), הכפתור צריך להראות שמשהו קורה. הדפוס הנפוץ: טקסט מתכווץ → spinner מופיע → כפתור disabled:
/* Loading state — כפתור מתכווץ ל-spinner */
.btn-loading {
position: relative;
min-width: 120px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-loading.is-loading {
width: 48px;
min-width: 48px;
height: 48px;
border-radius: 50%;
padding: 0;
pointer-events: none;
background: #818cf8;
}
.btn-loading.is-loading .btn-text {
opacity: 0;
}
.btn-loading .spinner {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
}
.btn-loading.is-loading .spinner {
opacity: 1;
}
.btn-loading .spinner::after {
content: '';
width: 20px;
height: 20px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
// שימוש: לחיצה → loading → success
async function handleSubmit(button) {
button.classList.add('is-loading');
try {
await sendData(); // הפעולה האסינכרונית
button.classList.remove('is-loading');
button.classList.add('is-success');
button.querySelector('.btn-text').textContent = '✓';
setTimeout(() => {
button.classList.remove('is-success');
button.querySelector('.btn-text').textContent = 'שלח';
}, 2000);
} catch (error) {
button.classList.remove('is-loading');
button.classList.add('is-error');
button.querySelector('.btn-text').textContent = 'שגיאה';
setTimeout(() => {
button.classList.remove('is-error');
button.querySelector('.btn-text').textContent = 'שלח';
}, 2000);
}
}
שלב 4: Success/Error states
אחרי ה-loading, צריך להראות תוצאה ברורה — הצלחה או שגיאה:
/* Success — ירוק + V */
.btn-loading.is-success {
background: #22c55e;
border-radius: 8px;
width: auto;
min-width: 120px;
padding: 12px 24px;
}
/* Error — אדום + shake */
.btn-loading.is-error {
background: #ef4444;
border-radius: 8px;
width: auto;
min-width: 120px;
padding: 12px 24px;
animation: shake 0.4s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
}
<button class="btn btn-ripple btn-loading"><span class="btn-text">שלח</span><span class="spinner"></span></button>GSAP גרסה — אנימציה חלקה יותר:
// GSAP button loading animation — חלק יותר מ-CSS
function animateButton(button) {
const tl = gsap.timeline();
const text = button.querySelector('.btn-text');
// שלב 1: כפתור מתכווץ לעיגול
tl.to(text, { opacity: 0, duration: 0.15 })
.to(button, {
width: 48,
borderRadius: '50%',
duration: 0.4,
ease: 'power2.inOut'
})
// שלב 2: spinner מופיע
.to(button.querySelector('.spinner'), {
opacity: 1,
duration: 0.2
});
return tl; // מחזיר timeline כדי להמשיך אחר כך
}
// Success animation
function showSuccess(button) {
const tl = gsap.timeline();
const text = button.querySelector('.btn-text');
tl.to(button.querySelector('.spinner'), { opacity: 0, duration: 0.15 })
.to(button, {
width: 'auto',
borderRadius: 8,
background: '#22c55e',
duration: 0.3,
ease: 'power2.out'
})
.set(text, { textContent: '✓ נשלח בהצלחה' })
.to(text, { opacity: 1, duration: 0.2 })
// חזרה ל-idle אחרי 2 שניות
.to(button, {
background: '#6366f1',
duration: 0.3,
delay: 2
})
.set(text, { textContent: 'שלח' });
return tl;
}
כשכפתור במצב loading, חייבים למנוע לחיצות נוספות. pointer-events: none ב-CSS מספיק לוויזואלי, אבל תמיד הוסיפו גם button.disabled = true ב-JavaScript. זה מבטיח שגם screen readers יודעים שהכפתור לא זמין, וגם שלחיצת Enter מהמקלדת לא תשלח שוב. מניעת double-submit היא לא רק UX — היא מניעת באגים.
טפסים הם המקום שבו מיקרו-אינטראקציות עושות את ההבדל הגדול ביותר. למה? כי טפסים דורשים שיתוף פעולה — המשתמש נותן מידע, והממשק צריך להגיד לו אם המידע תקין, מה חסר, ואיפה יש בעיה. בלי מיקרו-אינטראקציות, טפסים מרגישים כמו חור שחור — מקלידים, לוחצים "שלח", ומקווים לטוב. עם מיקרו-אינטראקציות, כל שדה מתקשר בזמן אמת.
Floating Label — ה-label שמתרומם:
אחד מדפוסי הטופס הנפוצים ביותר — ה-label מתחיל כ-placeholder בתוך השדה, ובזמן focus מתרומם למעלה ומתכווץ:
/* Floating label — ה-label שמתרומם */
.form-group {
position: relative;
margin-bottom: 24px;
}
.form-group input {
width: 100%;
padding: 16px 12px 8px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 16px;
background: transparent;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group label {
position: absolute;
right: 12px; /* RTL */
top: 50%;
transform: translateY(-50%);
font-size: 16px;
color: #94a3b8;
pointer-events: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: right; /* RTL */
}
/* כש-input מקבל focus או יש בו ערך — label מתרומם */
.form-group input:focus + label,
.form-group input:not(:placeholder-shown) + label {
top: 8px;
transform: translateY(0);
font-size: 12px;
color: #6366f1;
}
/* Focus ring */
.form-group input:focus {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
outline: none;
}
הטריק: ה-input צריך placeholder=" " (רווח) כדי ש-:not(:placeholder-shown) יעבוד. זה selector שבודק אם יש ערך בשדה — גם אחרי שה-focus עוזב.
Inline Validation — feedback בזמן אמת:
במקום לחכות ללחיצה על "שלח", כל שדה מתאמת בזמן אמת ומראה feedback ויזואלי. הדפוס: מקלידים → validation רץ → border משתנה + אייקון מופיע + הודעה:
/* Validation states */
.form-group.is-valid input {
border-color: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15);
}
.form-group.is-error input {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
}
/* Validation icon — מופיע עם אנימציה */
.form-group .validation-icon {
position: absolute;
left: 12px; /* RTL — אייקון בצד שמאל */
top: 50%;
transform: translateY(-50%) scale(0);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
/* spring-like bounce */
}
.form-group.is-valid .validation-icon {
transform: translateY(-50%) scale(1);
color: #22c55e;
}
.form-group.is-error .validation-icon {
transform: translateY(-50%) scale(1);
color: #ef4444;
}
/* Error message — מחליק מלמעלה */
.form-group .error-message {
font-size: 13px;
color: #ef4444;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: all 0.3s ease;
margin-top: 4px;
}
.form-group.is-error .error-message {
max-height: 40px;
opacity: 1;
}
// Inline validation — בודק בזמן אמת
const emailInput = document.querySelector('#email');
const emailGroup = emailInput.closest('.form-group');
emailInput.addEventListener('input', debounce(function() {
const value = this.value;
// מנקה מצבים קודמים
emailGroup.classList.remove('is-valid', 'is-error');
if (value.length === 0) return; // ריק — לא מציגים כלום
if (isValidEmail(value)) {
emailGroup.classList.add('is-valid');
} else if (value.includes('@')) {
// מתחילים להראות שגיאה רק אחרי @
emailGroup.classList.add('is-error');
emailGroup.querySelector('.error-message').textContent =
'כתובת אימייל לא תקינה';
}
}, 300)); // debounce 300ms — לא בודק על כל תו
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
Password Strength Meter — מד חוזק סיסמה:
מד חוזק סיסמה הוא דוגמה מושלמת למיקרו-אינטראקציה שנותנת feedback בזמן אמת ומשנה התנהגות — המשתמש משפר את הסיסמה בגלל ה-feedback:
/* Password strength meter */
.password-meter {
height: 4px;
background: #e2e8f0;
border-radius: 2px;
margin-top: 8px;
overflow: hidden;
}
.password-meter .fill {
height: 100%;
border-radius: 2px;
transition: width 0.4s ease, background 0.4s ease;
width: 0%;
}
.password-meter .fill.weak {
width: 25%;
background: #ef4444;
}
.password-meter .fill.fair {
width: 50%;
background: #f59e0b;
}
.password-meter .fill.good {
width: 75%;
background: #22c55e;
}
.password-meter .fill.strong {
width: 100%;
background: #16a34a;
}
Multi-step Form Progress — אינדיקטור שלבים:
טפסים ארוכים שמחולקים לשלבים צריכים progress indicator שמראה איפה המשתמש נמצא ומה נשאר:
/* Step progress indicator */
.step-progress {
display: flex;
justify-content: space-between;
position: relative;
margin-bottom: 32px;
}
.step-progress::before {
content: '';
position: absolute;
top: 16px;
right: 0; /* RTL */
left: 0;
height: 2px;
background: #e2e8f0;
}
.step-progress .progress-fill {
position: absolute;
top: 16px;
right: 0; /* RTL */
height: 2px;
background: #6366f1;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.step {
width: 32px;
height: 32px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: #64748b;
position: relative;
z-index: 1;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.step.active {
background: #6366f1;
color: white;
transform: scale(1.15);
}
.step.completed {
background: #22c55e;
color: white;
}
קריטריון הצלחה: הטופס נותן feedback ויזואלי מיידי על כל פעולה — focus, הקלדה, validation, submit.
ניווט הוא ה-backbone של כל אתר — והמיקרו-אינטראקציות שלו מתקשרות "איפה אתם" ו-"לאן אתם יכולים ללכת". בואו נבנה ארבעה דפוסי ניווט נפוצים.
Active Link Indicator — הקו שעוקב:
במקום סתם צבע שונה על הלינק הפעיל — קו תחתון שמחליק בצורה חלקה מלינק ללינק:
/* Sliding active indicator */
.nav-links {
display: flex;
gap: 24px;
position: relative;
}
.nav-link {
padding: 8px 0;
color: #64748b;
text-decoration: none;
transition: color 0.2s;
position: relative;
}
.nav-link:hover {
color: #1e293b;
}
/* הקו המחליק — pseudo-element שמונפש */
.nav-indicator {
position: absolute;
bottom: 0;
height: 2px;
background: #6366f1;
border-radius: 1px;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
// JavaScript — מזיז את ה-indicator ללינק הפעיל
const nav = document.querySelector('.nav-links');
const indicator = nav.querySelector('.nav-indicator');
const links = nav.querySelectorAll('.nav-link');
function moveIndicator(link) {
const { offsetLeft, offsetWidth } = link;
indicator.style.left = offsetLeft + 'px';
indicator.style.width = offsetWidth + 'px';
}
links.forEach(link => {
link.addEventListener('click', (e) => {
links.forEach(l => l.classList.remove('active'));
link.classList.add('active');
moveIndicator(link);
});
// גם hover מזיז את ה-indicator (וחוזר ב-mouseleave)
link.addEventListener('mouseenter', () => moveIndicator(link));
});
nav.addEventListener('mouseleave', () => {
const active = nav.querySelector('.nav-link.active');
if (active) moveIndicator(active);
});
Hamburger → X Morph — המבורגר שהופך ל-X:
שלושת הקווים שהופכים ל-X כשהתפריט נפתח — מיקרו-אינטראקציה קלאסית שמתקשרת state (תפריט פתוח/סגור):
/* Hamburger → X morph */
.hamburger {
width: 32px;
height: 24px;
position: relative;
cursor: pointer;
background: none;
border: none;
padding: 0;
}
.hamburger span {
display: block;
width: 100%;
height: 2px;
background: #1e293b;
border-radius: 1px;
position: absolute;
right: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.hamburger span:nth-child(1) { top: 0; }
.hamburger span:nth-child(2) { top: 11px; width: 75%; }
.hamburger span:nth-child(3) { top: 22px; }
/* Morph to X */
.hamburger.is-open span:nth-child(1) {
top: 11px;
transform: rotate(45deg);
}
.hamburger.is-open span:nth-child(2) {
opacity: 0;
transform: translateX(-10px);
}
.hamburger.is-open span:nth-child(3) {
top: 11px;
transform: rotate(-45deg);
}
הקו האמצעי מתכווץ ונעלם (opacity + translateX), בזמן שהעליון והתחתון נעים למרכז ומסתובבים ל-45 מעלות — יוצרים X. האנימציה לוקחת 300ms — מספיק כדי לראות את המעבר, לא מספיק כדי שיחסום.
Scroll Progress Bar — פס התקדמות קריאה:
פס דק בראש הדף שממלא את עצמו ככל שגוללים למטה — מראה כמה מהתוכן כבר קראתם:
/* Scroll progress bar */
.scroll-progress {
position: fixed;
top: 0;
right: 0; /* RTL */
height: 3px;
background: #6366f1;
z-index: 9999;
width: 0%;
transition: width 0.1s linear;
}
// JavaScript — מעדכן את הרוחב לפי מיקום הגלילה
const progressBar = document.querySelector('.scroll-progress');
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight
- window.innerHeight;
const progress = (scrollTop / docHeight) * 100;
progressBar.style.width = progress + '%';
}, { passive: true }); // passive לביצועים
Breadcrumb Animations — ניווט מדורג מונפש:
כשנכנסים לדף חדש, ה-breadcrumb מתעדכן עם אנימציה קטנה — הפריט החדש מחליק מימין (RTL) ומופיע:
/* Breadcrumb item animation */
.breadcrumb-item {
display: inline-flex;
align-items: center;
animation: breadcrumb-in 0.3s ease-out;
}
@keyframes breadcrumb-in {
from {
opacity: 0;
transform: translateX(10px); /* RTL — מימין */
}
to {
opacity: 1;
transform: translateX(0);
}
}
.breadcrumb-separator {
margin: 0 8px;
color: #94a3b8;
animation: breadcrumb-in 0.3s ease-out 0.1s both;
/* delay קל אחרי הפריט */
}
is-openToggles הם מיקרו-אינטראקציות בצורתם הטהורה ביותר — מצב אחד עובר למצב אחר, ויש אנימציה שמתקשרת את המעבר. בואו נבנה ארבעה דפוסים.
Toggle Switch — מתג on/off:
המתג הקלאסי — עיגול שמחליק ימינה/שמאלה עם שינוי צבע. פשוט מאוד, אבל הפרטים חשובים:
/* Toggle switch — גרסה RTL */
.toggle {
width: 52px;
height: 28px;
border-radius: 14px;
background: #cbd5e1;
position: relative;
cursor: pointer;
transition: background 0.3s ease;
border: none;
padding: 0;
}
.toggle::after {
content: '';
position: absolute;
width: 22px;
height: 22px;
border-radius: 50%;
background: white;
top: 3px;
right: 3px; /* RTL — מתחיל מימין */
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
/* spring easing — bounce קטן */
}
.toggle.is-on {
background: #6366f1;
}
.toggle.is-on::after {
transform: translateX(-24px); /* RTL — זז שמאלה */
}
/* כשלוחצים — העיגול נמתח קצת */
.toggle:active::after {
width: 26px;
}
שימו לב ל-spring easing (cubic-bezier(0.34, 1.56, 0.64, 1)) — העיגול קצת "קופץ" כשהוא מגיע לצד השני. ה-:active מרחיב את העיגול ל-26px — תחושת "מתיחה" כשלוחצים, כמו ב-iOS. הפרטים הקטנים האלה הם ההבדל בין toggle גנרי לבין toggle שמרגיש premium.
Theme Toggle — dark/light עם אייקון:
/* Sun/Moon theme toggle */
.theme-toggle {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #e2e8f0;
background: transparent;
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.3s;
}
.theme-toggle .icon {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.2s;
}
.theme-toggle .sun {
transform: rotate(0deg) scale(1);
}
.theme-toggle .moon {
transform: rotate(-90deg) scale(0);
opacity: 0;
}
/* Dark mode */
.theme-toggle.dark .sun {
transform: rotate(90deg) scale(0);
opacity: 0;
}
.theme-toggle.dark .moon {
transform: rotate(0deg) scale(1);
opacity: 1;
}
השמש מסתובבת 90 מעלות ומתכווצת, בזמן שהירח מסתובב מ-(-90) ל-0 וגדל. תנועה חד-כיוונית שמרגישה כמו סיבוב אחד — כאילו השמש הופכת לירח. האפקט עדין אבל ברור.
Accordion — פתיחה/סגירה חלקה:
/* Accordion — smooth open/close */
.accordion-item {
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 8px;
overflow: hidden;
}
.accordion-header {
padding: 16px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
background: white;
transition: background 0.2s;
}
.accordion-header:hover {
background: #f8fafc;
}
.accordion-header .arrow {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.accordion-item.is-open .accordion-header .arrow {
transform: rotate(180deg);
}
.accordion-content {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.accordion-item.is-open .accordion-content {
grid-template-rows: 1fr;
}
.accordion-content-inner {
overflow: hidden;
padding: 0 16px;
transition: padding 0.35s;
}
.accordion-item.is-open .accordion-content-inner {
padding: 0 16px 16px;
}
הטריק של grid-template-rows: 0fr → 1fr הוא הדרך המודרנית ב-CSS לאנימציית גובה מ-0 ל-auto. לא צריכים לדעת את הגובה מראש — CSS Grid מטפל בזה. נתמך בכל הדפדפנים המודרניים מ-2023.
Tabs — מעבר חלק בין תכנים:
/* Animated tabs */
.tabs-header {
display: flex;
position: relative;
border-bottom: 1px solid #e2e8f0;
}
.tab-button {
padding: 12px 24px;
background: none;
border: none;
cursor: pointer;
color: #64748b;
font-size: 14px;
font-weight: 500;
transition: color 0.2s;
}
.tab-button.active {
color: #6366f1;
}
/* Active tab indicator — מחליק */
.tabs-indicator {
position: absolute;
bottom: -1px;
height: 2px;
background: #6366f1;
border-radius: 1px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Tab content — fade + slide */
.tab-panel {
opacity: 0;
transform: translateY(8px);
transition: all 0.3s ease;
display: none;
}
.tab-panel.active {
display: block;
opacity: 1;
transform: translateY(0);
}
<button class="toggle" role="switch" aria-checked="false"></button>is-on ומעדכן aria-checked:active width — מ-26px ל-30px. מה קורה לתחושה?Cursor effects הם מיקרו-אינטראקציות שמגיבות לתנועת העכבר — לא רק ללחיצה או hover. הן יוצרות תחושה שהממשק "חי" ומגיב לנוכחות שלכם עוד לפני שלחצתם על משהו. שלושה דפוסים מרכזיים.
Custom Cursor Follower — עוקב סמן:
אלמנט שעוקב אחרי הסמן עם עיכוב קל (lerp) — יוצר תנועה חלקה ואורגנית. הסמן המותאם משתנה לפי ההקשר: גדל על כפתורים, מתכווץ על טקסט:
/* Custom cursor follower */
.cursor-follower {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #6366f1;
position: fixed;
pointer-events: none;
z-index: 9999;
transition: width 0.3s, height 0.3s, border-color 0.3s,
background 0.3s, transform 0.3s;
transform: translate(-50%, -50%);
mix-blend-mode: difference; /* אפקט מגניב על רקעים */
}
/* כשה-cursor מעל כפתור — גדל */
.cursor-follower.is-hovering {
width: 50px;
height: 50px;
background: rgba(99, 102, 241, 0.1);
}
/* כשלוחצים — מתכווץ */
.cursor-follower.is-clicking {
width: 15px;
height: 15px;
background: #6366f1;
}
// Cursor follower — GSAP lerp לתנועה חלקה
const cursor = document.querySelector('.cursor-follower');
let mouseX = 0, mouseY = 0;
let cursorX = 0, cursorY = 0;
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
// Lerp — מחליק את התנועה (60fps)
function animateCursor() {
// lerp factor — 0.15 = חלק, 0.5 = מהיר
cursorX += (mouseX - cursorX) * 0.15;
cursorY += (mouseY - cursorY) * 0.15;
cursor.style.left = cursorX + 'px';
cursor.style.top = cursorY + 'px';
requestAnimationFrame(animateCursor);
}
animateCursor();
// שינוי state על כפתורים וקישורים
document.querySelectorAll('a, button').forEach(el => {
el.addEventListener('mouseenter', () =>
cursor.classList.add('is-hovering'));
el.addEventListener('mouseleave', () =>
cursor.classList.remove('is-hovering'));
});
document.addEventListener('mousedown', () =>
cursor.classList.add('is-clicking'));
document.addEventListener('mouseup', () =>
cursor.classList.remove('is-clicking'));
Magnetic Button — כפתור מגנטי:
כפתור שנמשך לכיוון הסמן כשמתקרבים — כאילו יש מגנט. אפקט עדין (5-15px תזוזה) שגורם ללחיצה להרגיש "בלתי נמנעת":
// Magnetic button — כפתור שנמשך לסמן
document.querySelectorAll('.magnetic-btn').forEach(btn => {
btn.addEventListener('mousemove', (e) => {
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
// maxMove — כמה הכפתור זז (פיקסלים)
const maxMove = 10;
const moveX = (x / rect.width) * maxMove;
const moveY = (y / rect.height) * maxMove;
gsap.to(btn, {
x: moveX,
y: moveY,
duration: 0.3,
ease: 'power2.out'
});
});
btn.addEventListener('mouseleave', () => {
gsap.to(btn, {
x: 0,
y: 0,
duration: 0.5,
ease: 'elastic.out(1, 0.3)'
});
});
});
כשהעכבר עוזב — elastic.out מחזיר את הכפתור למקום עם "רטט" קטן, כמו גומייה שמשתחררת. ה-maxMove של 10px מספיק עדין שלא ירגיש מוזר אבל מספיק ניכר שתרגישו את המשיכה.
Hover Reveal — תוכן שמתגלה ב-hover:
/* Card hover reveal — תמונה מתגלה */
.reveal-card {
position: relative;
overflow: hidden;
border-radius: 12px;
cursor: pointer;
}
.reveal-card img {
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.reveal-card:hover img {
transform: scale(1.05);
}
.reveal-card .overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(0,0,0,0.8) 0%,
transparent 60%
);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 24px;
transform: translateY(20%);
opacity: 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.reveal-card:hover .overlay {
transform: translateY(0);
opacity: 1;
}
.reveal-card .overlay h3 {
transform: translateY(10px);
transition: transform 0.4s 0.1s ease;
}
.reveal-card:hover .overlay h3 {
transform: translateY(0);
}
אפקטי cursor מותאמים רלוונטיים רק ל-desktop עם עכבר. במובייל אין cursor — ואפקטי hover לא עובדים כמו ב-desktop (tap שם = hover + click ביחד). תמיד בדקו שה-CSS שלכם כולל @media (hover: hover) and (pointer: fine) לפני שמפעילים cursor effects. בלי בדיקה זו — תראו באגים מוזרים במובייל כשרכיבים "נתקעים" במצב hover.
עד כאן התמקדנו בויזואלי. עכשיו מוסיפים שכבה שרוב המפתחים מתעלמים ממנה — צלילים. חשבו על הצליל הקטן שנשמע כשאתם שולחים הודעה ב-iMessage, או ה-"click" של keyboard shortcuts ב-Mac, או הצליל של pull-to-refresh ב-Twitter. צלילים מוסיפים מימד שלם ל-feedback loop — אישור שמגיע דרך האוזניים, לא רק דרך העיניים.
Web Audio API — המנוע מאחורי הקלעים:
Web Audio API הוא ממשק JavaScript שמאפשר לייצר, לעבד, ולנגן צלילים בדפדפן. הוא עובד עם graph של nodes — כל node מעבד את האודיו ומעביר הלאה. אבל לשימוש בסיסי — מספיק להכיר שלושה דברים:
// יצירת AudioContext — המנוע
const audioCtx = new (window.AudioContext
|| window.webkitAudioContext)();
// ניגון קובץ אודיו
async function playSound(url) {
// טוענים את הקובץ
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
// יוצרים source → gain → output
const source = audioCtx.createBufferSource();
source.buffer = audioBuffer;
const gain = audioCtx.createGain();
gain.gain.value = 0.3; // volume — 30%
source.connect(gain);
gain.connect(audioCtx.destination);
source.start(0);
}
// שימוש
document.querySelector('#send-btn').addEventListener('click', () => {
playSound('/sounds/click.mp3');
});
User Gesture Requirement — הדפדפן חוסם צלילים:
דפדפנים מודרניים חוסמים AudioContext עד שהמשתמש עושה אינטראקציה (click, tap, keypress). זה למנוע אתרים שמנגנים צלילים מבלי לשאול. בפועל: ה-AudioContext נוצר במצב "suspended" ועובר ל-"running" רק אחרי user gesture:
// Pattern: resume AudioContext on first interaction
const audioCtx = new AudioContext();
async function ensureAudioReady() {
if (audioCtx.state === 'suspended') {
await audioCtx.resume();
}
}
// מפעילים בכל interaction ראשון
document.addEventListener('click', ensureAudioReady, { once: true });
document.addEventListener('keydown', ensureAudioReady, { once: true });
document.addEventListener('touchstart', ensureAudioReady, { once: true });
Oscillator — צלילים סינתטיים (בלי קבצים):
אפשר ליצור צלילים מאפס — בלי קבצי אודיו — באמצעות oscillator (מתנד). מתאים לצלילי UI קצרים כמו click, beep, notification:
// יצירת צליל "click" סינתטי
function playClick() {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine'; // סוג גל: sine, square, triangle, sawtooth
osc.frequency.value = 800; // תדר ב-Hz — 800 = צליל גבוה
gain.gain.value = 0.1; // volume נמוך
// Envelope — הצליל מתחיל ונגמר מהר
const now = audioCtx.currentTime;
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
// צליל דועך תוך 100ms
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.1); // עוצר אחרי 100ms
}
// צליל "success" — שני טונים עולים
function playSuccess() {
[600, 900].forEach((freq, i) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.value = 0;
const time = audioCtx.currentTime + i * 0.12;
gain.gain.setValueAtTime(0.08, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.15);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(time);
osc.stop(time + 0.15);
});
}
// צליל "error" — טון יורד
function playError() {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(200,
audioCtx.currentTime + 0.2);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001,
audioCtx.currentTime + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.3);
}
playClick() — שמעתם צליל?| סוג גל | צליל | שימוש |
|---|---|---|
| sine | טהור, עגול, עדין | צלילי UI רכים, notifications, hover |
| triangle | מעט "חד" יותר מ-sine | clicks, toggles, tab switches |
| square | חד, "דיגיטלי", 8-bit | error beeps, alerts, retro style |
| sawtooth | חזק, "מנסרי", aggressive | warnings, game sounds, שימוש זהיר ב-UI |
ביצירת צלילים לאתר, הכלל הראשון הוא: less is more. באמת. הרבה פחות ממה שאתם חושבים. צליל באתר צריך להיות כמו בשמים יקר — מורגש רק כשמתקרבים מאוד. רוב המשתמשים לא מצפים לצלילים באתרים (בניגוד לאפליקציות), ולכן כל צליל חייב להצדיק את קיומו.
חמש קטגוריות של צלילי UI:
עקרונות עיצוב צלילי לאתרים:
איפה מוצאים צלילים?
הטעות הנפוצה ביותר בצלילי web: volume גבוה מדי. המשתמש יושב עם אוזניות, מנגן Spotify ברקע, ופתאום צליל של האתר שלכם מפוצץ לו אוזניים. כל צליל UI צריך להיות ב-volume 0.03-0.15. אם אתם בודקים צליל ואומרים "אני בקושי שומע את זה" — מעולה, זה בדיוק הנכון. שקט = מקצועי. חזק = מעצבן.
Web Audio API עובד, אבל הוא low-level — הרבה boilerplate code לכל צליל. בפרויקט אמיתי, משתמשים בספריות שעוטפות אותו ומספקות API פשוט, cross-browser support, ו-features כמו sprites, pooling, ו-spatial audio. שתי הספריות המובילות: Howler.js ל-playback ו-Tone.js ליצירה סינתטית.
Howler.js — הספרייה הסטנדרטית לצלילי web:
Howler.js (28K+ stars ב-GitHub) מספקת API פשוט לניגון צלילים. תומכת ב-audio sprites (צליל אחד שמכיל כמה צלילים), auto-detection של פורמט, pooling (מנגן את אותו צליל כמה פעמים במקביל), ו-fallback ל-HTML5 Audio כשצריך:
// Howler.js — הגדרת צלילי UI
// CDN: <script src="https://cdnjs.cloudflare.com/ajax/libs/
// howler/2.2.4/howler.min.js"></script>
// Audio Sprite — כל הצלילים בקובץ אחד
const uiSounds = new Howl({
src: ['/sounds/ui-sprites.webm', '/sounds/ui-sprites.mp3'],
sprite: {
click: [0, 100], // מתחיל ב-0ms, אורך 100ms
toggle: [200, 150], // מתחיל ב-200ms, אורך 150ms
success: [500, 300], // מתחיל ב-500ms, אורך 300ms
error: [1000, 250], // מתחיל ב-1000ms, אורך 250ms
whoosh: [1500, 200], // מתחיל ב-1500ms, אורך 200ms
notify: [2000, 400], // מתחיל ב-2000ms, אורך 400ms
},
volume: 0.1, // ברירת מחדל — שקט
});
// שימוש
function playUI(name) {
if (!isMuted) {
uiSounds.play(name);
}
}
// חיבור לאירועים
document.querySelector('#submit-btn')
.addEventListener('click', () => playUI('click'));
document.querySelector('.toggle')
.addEventListener('click', () => playUI('toggle'));
היתרון של Audio Sprite: request אחד במקום 6. במקום 6 קבצי אודיו נפרדים — קובץ אחד שנטען פעם אחת. Howler.js יודע לנגן רק את החלק הרלוונטי לפי ה-sprite map.
Tone.js — צלילים סינתטיים מתקדמים:
Tone.js (13K+ stars) היא ספרייה ליצירת צלילים בקוד — synthesizers, effects, sequencers. מושלמת ליצירת צלילי UI ייחודיים שלא דורשים קבצי אודיו:
// Tone.js — צלילי UI סינתטיים
// CDN: <script src="https://cdnjs.cloudflare.com/ajax/libs/
// tone/14.8.49/Tone.js"></script>
// Synth בסיסי לצלילי UI
const synth = new Tone.Synth({
oscillator: { type: 'sine' },
envelope: {
attack: 0.005, // כמה מהר הצליל מתחיל
decay: 0.1, // כמה מהר הוא דועך
sustain: 0, // לא נשאר — צליל קצר
release: 0.1 // כמה מהר הוא נגמר לגמרי
},
volume: -20 // dB — שקט
}).toDestination();
// צלילי UI שונים
const sounds = {
click: () => synth.triggerAttackRelease('C5', '32n'),
success: () => {
synth.triggerAttackRelease('E5', '16n', Tone.now());
synth.triggerAttackRelease('G5', '16n', Tone.now() + 0.1);
},
error: () => {
synth.triggerAttackRelease('A3', '8n', Tone.now());
},
toggle: () => synth.triggerAttackRelease('D5', '32n'),
hover: () => {
const s = new Tone.Synth({ volume: -30 }).toDestination();
s.triggerAttackRelease('G6', '64n');
}
};
// חיבור
document.querySelector('.btn').addEventListener('click', () => {
Tone.start(); // User gesture — required once
sounds.click();
});
Mute Toggle — מתג השתקה:
כל אתר שמנגן צלילים חייב mute toggle ברור. ואת ההעדפה צריך לזכור:
// Mute toggle with localStorage persistence
let isMuted = localStorage.getItem('sound-muted') === 'true';
function updateMuteUI() {
const btn = document.querySelector('.mute-toggle');
btn.setAttribute('aria-label',
isMuted ? 'הפעל צלילים' : 'השתק צלילים');
btn.textContent = isMuted ? '🔇' : '🔊';
// Howler: global mute
Howler.mute(isMuted);
}
document.querySelector('.mute-toggle').addEventListener('click', () => {
isMuted = !isMuted;
localStorage.setItem('sound-muted', isMuted);
updateMuteUI();
// Feedback — לנגן צליל קטן כשמבטלים mute
if (!isMuted) playUI('toggle');
});
// Init
updateMuteUI();
קריטריון הצלחה: 5 צלילים שעובדים, mute שנשמר, volume slider.
צלילים ואנימציות חזקים לבד. יחד — הם יוצרים חוויה שמרגישה פיזית. חשבו על הלייק של Twitter: הלב גדל, מתכווץ, ופורח לזיקוקים — ובדיוק באותו רגע נשמע "pop" קטן. שני חושים שמאשרים את אותו אירוע — המוח מפרש את זה כ-"אמיתי". הסנכרון הזה הוא מה שמפריד בין חוויה טובה לחוויה מעולה.
עקרון הסנכרון: הצליל מלווה את "הרגע"
לכל אנימציה יש "רגע" — הנקודה שבה הפעולה הויזואלית הכי אינטנסיבית. ב-bounce — הרגע שמגיע ל-peak. ב-ripple — הרגע שמתחיל להתפשט. ב-toggle — הרגע ש"נוחת" בצד השני. הצליל צריך לנגן בדיוק ב-"רגע" הזה — לא לפני, לא אחרי. עיכוב של 50ms כבר מורגש.
// Pattern 1: GSAP onStart — צליל בתחילת אנימציה
gsap.to('.card', {
scale: 1.05,
duration: 0.3,
ease: 'power2.out',
onStart: () => playUI('whoosh')
});
// Pattern 2: GSAP callback at specific point
const tl = gsap.timeline();
tl.to('.button', { scale: 0.95, duration: 0.1 })
.to('.button', {
scale: 1,
duration: 0.3,
ease: 'elastic.out(1, 0.3)',
onStart: () => playUI('click') // צליל ברגע ה-bounce
})
.to('.checkmark', {
scale: 1,
opacity: 1,
duration: 0.3,
onStart: () => playUI('success') // צליל כשה-V מופיע
});
// Pattern 3: CSS animation + animationstart event
element.addEventListener('animationstart', (e) => {
if (e.animationName === 'shake') playUI('error');
});
// Pattern 4: Intersection Observer + sound
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
playUI('whoosh');
// Start animation too
entry.target.classList.add('animate-in');
}
});
}, { threshold: 0.5 });
document.querySelectorAll('.reveal-section')
.forEach(el => observer.observe(el));
Full Example: Toggle עם צליל מסונכרן
// Toggle switch עם צליל מסונכרן מושלם
function animatedToggle(toggleEl) {
const isOn = toggleEl.classList.toggle('is-on');
const knob = toggleEl.querySelector('.knob');
// אנימציה: knob מחליק + stretch
const tl = gsap.timeline();
tl.to(knob, {
scaleX: 1.3,
duration: 0.1,
ease: 'power2.in'
})
.to(knob, {
x: isOn ? -24 : 0, // RTL
duration: 0.2,
ease: 'power2.inOut',
onComplete: () => {
// הצליל מתנגן כשה-knob "נוחת"
if (isOn) {
playUI('toggle'); // צליל "on" — טון עולה
} else {
// צליל "off" — טון יורד (pitch shift)
uiSounds.rate(0.8); // pitch נמוך יותר
playUI('toggle');
uiSounds.rate(1.0); // חזרה לנורמלי
}
}
})
.to(knob, {
scaleX: 1,
duration: 0.15,
ease: 'elastic.out(1, 0.5)'
});
// Background color transition
gsap.to(toggleEl, {
backgroundColor: isOn ? '#6366f1' : '#cbd5e1',
duration: 0.3
});
// Update accessibility
toggleEl.setAttribute('aria-checked', isOn);
}
// Haptic feedback (mobile)
function haptic(duration = 10) {
if ('vibrate' in navigator) {
navigator.vibrate(duration);
}
}
Full Example: Form Submit עם סאונד
// Form submit — visual + audio + haptic
async function submitWithFeedback(form, button) {
// Phase 1: Click feedback
playUI('click');
haptic(10);
// Phase 2: Loading
const loadingTl = gsap.timeline();
loadingTl.to(button, {
width: 48, borderRadius: 24,
duration: 0.3,
onStart: () => {
// Subtle loop sound while loading (optional)
// loadingLoop = uiSounds.play('loading-loop');
}
});
try {
const result = await submitForm(form);
// Phase 3: Success
loadingTl.reverse();
await loadingTl; // Wait for reverse
playUI('success');
haptic(50); // Stronger haptic for success
gsap.to(button, {
backgroundColor: '#22c55e',
duration: 0.3,
yoyo: true,
repeat: 1,
repeatDelay: 1
});
} catch (error) {
// Phase 3: Error
loadingTl.reverse();
playUI('error');
haptic([50, 30, 50]); // Pattern: buzz-pause-buzz
gsap.to(button, {
x: [-6, 6, -4, 4, 0],
duration: 0.4,
ease: 'power2.out'
});
gsap.to(button, {
backgroundColor: '#ef4444',
duration: 0.3,
yoyo: true,
repeat: 1,
repeatDelay: 1
});
}
}
| אירוע | אנימציה | צליל | Timing |
|---|---|---|---|
| Click button | scale 0.95→1 + ripple | click (50ms) | onStart של ה-bounce back |
| Toggle on/off | knob slides + stretch | toggle (100ms) | onComplete של ה-slide |
| Success | V + green flash | two-tone up (200ms) | onStart של ה-V appear |
| Error | shake + red flash | low buzz (200ms) | onStart של ה-shake |
| Page transition | slide/fade out→in | whoosh (150ms) | midpoint של ה-transition |
| Notification | slide in + bounce | chime (300ms) | onComplete של ה-bounce |
מיקרו-אינטראקציות צריכות לעבוד לכולם — גם למשתמשים עם רגישות לתנועה, גם למשתמשים עם screen readers, וגם למשתמשים שמנווטים במקלדת בלבד. שלוש שכבות של נגישות שחובה ליישם.
1. prefers-reduced-motion — כיבוד העדפת המשתמש:
חלק מהמשתמשים סובלים מ-vestibular disorders — רגישות לתנועה שגורמת לסחרחורת, בחילה, או כאבי ראש. מערכות ההפעלה מאפשרות להגדיר "הפחתת תנועה", ו-CSS יודע לזהות את זה:
/* ברירת מחדל — אנימציות מלאות */
.btn {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* reduced-motion — מינימום אנימציה */
@media (prefers-reduced-motion: reduce) {
/* מבטלים transitions ארוכים */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
/* שומרים על opacity transitions — כי הם לא גורמים לסחרחורת */
.btn {
transition: opacity 0.15s ease, background-color 0.15s ease;
}
/* מחליפים slide animations ב-fade */
.accordion-content {
transition: opacity 0.15s ease;
}
/* scroll progress bar — עדיין עובד, רק בלי animation */
.scroll-progress {
transition: none;
}
}
// JavaScript — בודקים ומגיבים
const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
function shouldAnimate() {
return !prefersReduced.matches;
}
// GSAP — מכבים אנימציות
if (!shouldAnimate()) {
gsap.globalTimeline.timeScale(100);
// מאיץ את כל האנימציות — מעשית מדלג עליהן
}
// מאזינים לשינויים בזמן אמת
prefersReduced.addEventListener('change', () => {
// המשתמש שינה את ההעדפה תוך כדי שימוש
if (prefersReduced.matches) {
gsap.globalTimeline.timeScale(100);
} else {
gsap.globalTimeline.timeScale(1);
}
});
2. Screen Reader Support — aria-live ו-role:
מיקרו-אינטראקציות ויזואליות לא נראות ל-screen readers. צריכים להוסיף ARIA attributes שמתקשרים את אותו מידע:
// כפתור loading → success: מעדכנים aria-live
const statusRegion = document.querySelector('[aria-live="polite"]');
async function submitForm() {
button.setAttribute('aria-busy', 'true');
button.textContent = 'שולח...';
statusRegion.textContent = 'שולח את הטופס...';
try {
await sendData();
button.setAttribute('aria-busy', 'false');
button.textContent = '✓ נשלח';
statusRegion.textContent = 'הטופס נשלח בהצלחה';
} catch (err) {
button.setAttribute('aria-busy', 'false');
statusRegion.textContent = 'שגיאה בשליחת הטופס: ' + err.message;
// focus on error message for screen readers
statusRegion.focus();
}
}
// Toggle — aria-checked
toggle.addEventListener('click', () => {
const isOn = toggle.classList.toggle('is-on');
toggle.setAttribute('aria-checked', isOn);
// Screen reader יקרא: "מתג, מופעל/כבוי"
});
3. Focus Management — ניווט מקלדת:
/* Focus visible — ring ברור רק למקלדת */
*:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* מוודאים שכל interactive element focusable */
.toggle[role="switch"] { /* tabindex="0" ב-HTML */ }
.accordion-header[role="button"] { /* tabindex="0" ב-HTML */ }
/* Focus trap for modals */
// Keyboard support for custom components
toggle.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggle.click(); // מפעיל את אותה פונקציה כמו mouse click
}
});
// Accordion — Space/Enter to toggle, Arrow keys to navigate
accordionHeaders.forEach((header, index) => {
header.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
accordionHeaders[index + 1]?.focus();
break;
case 'ArrowUp':
e.preventDefault();
accordionHeaders[index - 1]?.focus();
break;
case 'Home':
e.preventDefault();
accordionHeaders[0].focus();
break;
case 'End':
e.preventDefault();
accordionHeaders[accordionHeaders.length - 1].focus();
break;
}
});
});
(1) צלילים אף פעם לא יהיו הדרך היחידה לתקשר מידע. אם צליל success מופעל — חייב להיות גם V ירוק ויזואלי וגם aria-live announcement. (2) צלילים אוטומטיים (ambient, background) חייבים להתחיל כבויים. screen readers עלולים לא לזהות שיש צליל ולא למצוא את כפתור ה-mute. הצלילים יפריעו לשמיעת ה-screen reader.
מיקרו-אינטראקציות הן מקום מושלם לשימוש ב-AI — הדפוסים ברורים, הקוד קצר, ו-AI כותב אותם היטב. הנה חמישה פרומפטים מוכנים ל-copy-paste:
פרומפט 1: ספריית מיקרו-אינטראקציות לכפתורים
Build a button micro-interaction library in vanilla CSS + JS:
- 4 button variants: primary, secondary, ghost, danger
- Each button has these states with smooth transitions:
idle → hover (lift + shadow) → active (press down + scale 0.97) →
focus-visible (ring)
- Add click ripple effect (Material Design style) that originates
from click position
- Add loading state: button morphs to circle with spinner
- Add success state: green + checkmark icon
- Add error state: red + shake animation
- All transitions use cubic-bezier(0.4, 0, 0.2, 1)
- Support prefers-reduced-motion
- RTL direction
- Show all states in a demo page
פרומפט 2: טופס עם inline validation מונפש
Create a registration form with animated micro-interactions:
- Fields: name, email, password, confirm password
- Floating labels that animate up on focus (CSS only)
- Real-time inline validation with:
- Green border + checkmark (scale bounce) on valid
- Red border + X + error message (slide down) on invalid
- Password strength meter (4 levels with color transition)
- Submit button with: ripple → loading spinner → success/error
- Error fields shake (translateX) when submitting with errors
- Use vanilla CSS transitions + minimal JS
- prefers-reduced-motion: replace all motion with opacity fades
- Add proper ARIA: aria-invalid, aria-describedby, aria-live
- RTL layout, Hebrew labels
פרומפט 3: Navigation micro-interactions
Build a responsive navigation bar with micro-interactions:
- Desktop: horizontal nav with sliding active indicator that
moves between links on click/hover
- Mobile: hamburger icon that morphs to X (CSS transition)
- Scroll progress bar at top (3px, fills as user scrolls)
- On scroll down: nav shrinks (height 80→60px, shadow appears)
- On scroll up: nav expands back
- Active link determined by scroll position (intersection observer)
- Smooth indicator slide uses cubic-bezier easing
- Use vanilla JS + CSS only (no frameworks)
- Support keyboard navigation (arrow keys, tab)
- RTL direction
פרומפט 4: Toggle switches עם צלילים
Create 3 animated toggle switches with sound:
1. Standard toggle: knob slides with spring easing + stretch on press
2. Theme toggle: sun/moon icons that rotate and cross-fade,
entire page transitions between light/dark
3. Accordion with smooth height animation (grid-template-rows trick)
- Add Web Audio API sounds using oscillators (no audio files):
- Toggle ON: ascending two-tone sine (600Hz → 800Hz)
- Toggle OFF: descending tone (500Hz → 300Hz)
- Accordion open: soft whoosh (noise burst)
- Mute toggle button with localStorage persistence
- All toggles update aria-checked / aria-expanded
- prefers-reduced-motion disables spring bounce,
keeps instant state change
- RTL layout
פרומפט 5: Cursor effects + magnetic buttons
Build an interactive cursor system for a portfolio site:
- Custom cursor follower (20px circle, mix-blend-mode: difference)
- Cursor follows mouse with lerp (0.15 factor, requestAnimationFrame)
- On hovering links/buttons: cursor grows to 50px with background
- On hovering images: cursor shows "View" text
- On click: cursor shrinks briefly (feedback)
- 3 magnetic buttons that pull toward cursor (max 12px offset)
- Buttons return with elastic.out easing on mouse leave
- Card hover reveals: image scales 1.05, overlay slides up with
staggered text
- Only activate on @media (hover: hover) and (pointer: fine)
- Use GSAP for all animations
- Include fallback for touch devices (no cursor effects)
- RTL aware
הדרך הטובה ביותר ללמוד מיקרו-אינטראקציות היא לנתח את מי שעושים את זה הכי טוב. ארבע חברות שהמיקרו-אינטראקציות שלהן מהוות Gold Standard בתעשייה.
1. Stripe Checkout — precision feedback
ממשק התשלום של Stripe הוא אחד הממשקים המלוטשים ביותר באינטרנט. הפרטים שעושים את זה:
מה ללמוד מ-Stripe: כל מיקרו-אינטראקציה משרתת הפחתת חרדה. תשלום = פעולה רגישה. כל feedback אומר "הכל בסדר, אנחנו מטפלים בזה". האנימציות קצרות (100-300ms), עדינות, ותמיד פונקציונליות.
2. Linear — satisfying completions
Linear (אפליקציית project management) ידועה במיקרו-אינטראקציות שגורמות לעבודה להרגיש מספקת:
מה ללמוד מ-Linear: מיקרו-אינטראקציות שהופכות עבודה שגרתית ל-satisfying. כל "done" מרגיש כמו הישג קטן. זה לא רק UX — זה gamification עדינה שמעודדת פרודוקטיביות.
3. Apple — the toggle standard
ה-toggle switch של iOS הוא כנראה המיקרו-אינטראקציה הכי מחוקה בהיסטוריה. מה הופך אותו מושלם:
מה ללמוד מ-Apple: Physicality. רכיבי UI שמתנהגים כמו אובייקטים פיזיים — עם מסה, תאוצה, ותגובה ללחיצה. זה מה שהופך אינטראקציה ל-"מרגישה נכון".
4. Notion — contextual intelligence
Notion מצטיינת במיקרו-אינטראקציות שמגיבות ל-הקשר:
מה ללמוד מ-Notion: מיקרו-אינטראקציות contextual — שמופיעות רק כשהן רלוונטיות ונעלמות כשלא. זה לא על "כמה אנימציות" אלא על "אנימציות בדיוק בזמן הנכון".
קריטריון הצלחה: גרסה עובדת שמרגישה 80% מהמקור — ואתם מבינים מה חסר ל-20% הנותרים.
קריטריון הצלחה: 5 רכיבים עובדים עם אנימציות, צלילים, נגישות, ו-reduced-motion. זו ספרייה שתוכלו להשתמש בה בפרויקטים אמיתיים.
| שלב | פעולה | זמן |
|---|---|---|
| 1. מיפוי מצבים | רשמו את כל המצבים של הרכיב: idle, hover, active, focus, loading, success, error, disabled. כל מעבר = הזדמנות למיקרו-אינטראקציה | 3 דקות |
| 2. CSS בסיסי | הוסיפו transitions לכל מעבר מצב. התחילו עם color, background, box-shadow, transform. ודאו easing מתאים | 5 דקות |
| 3. אנימציות מורכבות | אם צריך multi-step (loading → success) — GSAP timeline. אם צריך spring — spring easing | 10 דקות |
| 4. צליל (אופציונלי) | האם הרכיב צריך צליל? רק אם הוא פעולה משמעותית (submit, toggle, notification). בחרו צליל, סנכרנו | 5 דקות |
| 5. נגישות | ARIA attributes, keyboard support, focus-visible, aria-live. בדקו עם screen reader | 5 דקות |
| 6. reduced-motion | הוסיפו @media (prefers-reduced-motion: reduce). החליפו בפעולות מיידיות או opacity fades | 3 דקות |
| 7. mobile | בדקו touch: hover לא נתקע, toggle עובד, אין cursor effects. הוסיפו @media (hover: hover) לאפקטי cursor | 5 דקות |
| 8. test | בדקו בכל המצבים: mouse + keyboard + screen reader + reduced-motion + mobile. תקנו edge cases | 10 דקות |
בפרק הזה למדתם את הפרטים הקטנים — מיקרו-אינטראקציות שנותנות feedback, מתקשרות state, ויוצרות delight. למדתם גם להוסיף צלילים — שכבה שמשנה את החוויה מוויזואלית בלבד למרובת-חושים. עכשיו יש לכם את כל הכלים: CSS animations, GSAP, ScrollTrigger, Motion, Lenis, SVG, Lottie, Rive, Canvas, 3D, מיקרו-אינטראקציות, וצלילים. אבל כלים בלי תכנון = כאוס. בפרק 13 תלמדו תזמור אנימציות — איך לתכנן motion system שלם לאתר, איך לוודא שכל האנימציות עובדות יחד בהרמוניה, ואיך לבנות animation style guide שמבטיח עקביות. מ-"אוסף אנימציות" ל-"חוויה מתוזמרת".