הבעיה
סימון שדות חובה במצב שגיאה מיד עם טעינת הדף עלול לבלבל. באופן אידיאלי, שדה חובה צריך להיראות "לא תקין" רק אם המשתמש ניסה למלא אותו ונכשל.
הפתרון
הפסאודו-מחלקה :user-invalid פותרת את הבעיה הזו בצורה מושלמת. אם מדובר בשדה חובה, לא תהיה התאמה בטעינת דף. ההתאמה תתבצע רק אם:
- המשתמש יוצר אינטראקציה עם השדה (למשל, מקליד תו ומוחק אותו) ואז יוצא ממנו (מטשטש אותו) ומשאיר אותו ריק.
- המשתמש מנסה לשלוח את הטופס כשהשדה ריק.
אסטרטגיית הטמעה
- אילוץ HTML: מוסיפים את המאפיין
requiredלקלט. - משוב חזותי: משתמשים ב-
:user-invalidכדי לעצב את הגבול באדום ולהציג טקסט עזר "חובה". - תזמון: הסתמכו על התזמון המקורי של הדפדפן למשוב חזותי. כבר לא צריך פונקציות טיפול של
onBlurכדי להוסיף מחלקה שלtouched, אבל עדיין צריך קצת JavaScript כדי לסנכרן מאפייני ARIA (ראו בהמשך).
מדריך הטמעה
1. מבנה HTML
<form id="feedback-form">
<div class="field">
<label for="full-name">Full Name</label>
<input
type="text"
id="full-name"
name="full-name"
required
aria-errormessage="name-error"
>
<!-- MANDATORY: Include an icon or distinct non-color indicator alongside error text -->
<div id="name-error" class="error-msg">
<span aria-hidden="true">❌</span> This field is required.
</div>
</div>
</form>
2. CSS
.error-msg {
display: none;
color: #d93025;
font-size: 0.875rem;
margin-top: 0.25rem;
}
/*
Only highlight empty required fields AFTER the user visits them.
MANDATORY: Provide multiple indicators (border shift + helper text/icon) to avoid color-only state communication.
*/
input:user-invalid {
border-color: #d93025;
background-color: #fce8e6;
}
input:user-invalid + .error-msg {
display: block;
}
/* Optional: Subtle indicator for required fields that are valid */
input:required:user-valid {
border-color: #188038;
border-width: 2px;
}
3. סנכרון מצב JavaScript
חובה: מכיוון ש-:user-invalid הוא מצב חזותי, חובה לספק גשר JavaScript כדי לסנכרן את aria-invalid="true" באופן דינמי לטכנולוגיות מסייעות כשמשתמש מטשטש שדה לא חוקי או מנסה לשלוח.
const form = document.getElementById('feedback-form');
const syncAriaInvalid = (input) => {
if (!input.checkValidity()) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
// Sync on blur when a user finishes interacting
form.addEventListener('blur', (e) => {
if (e.target.matches('input[required]')) {
syncAriaInvalid(e.target);
}
}, true);
// Sync all required fields when submission is attempted
form.addEventListener('submit', () => {
form.querySelectorAll('input[required]').forEach(syncAriaInvalid);
});
// Remove error state immediately upon correction
form.addEventListener('input', (e) => {
if (e.target.matches('input[required]') && e.target.checkValidity()) {
e.target.removeAttribute('aria-invalid');
}
});
תמיכה בדפדפנים וחלופות
תמיכה בדפדפנים ובמצבי חוסר זמינות של :user-valid ו- :user-invalid
CSS חלופי
input:user-invalid,
input.user-invalid-fallback {
border-color: #d93025;
background-color: #fce8e6;
}
input:user-invalid + .error-msg,
input.user-invalid-fallback + .error-msg {
display: block;
}
גיבוי JavaScript
משתמשים בכלי שניתן לשימוש חוזר שעוקב אחרי מצב האינטראקציה באמצעות WeakMap. כך אפשר להימנע מ"זיהום" של ה-DOM עם מאפייני נתונים או מחלקות "מלוכלכות".
const UserInvalidFallback = (() => {
const dirtyState = new WeakMap();
const updateState = (input) => {
const isValid = input.checkValidity();
// Update both visual and ARIA state
input.classList.toggle('user-invalid-fallback', !isValid);
input.classList.toggle('user-valid-fallback', isValid);
if (!isValid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
const handleEvent = (event) => {
const input = event.target;
if (event.type === 'reset') {
const controls = input.elements || [];
for (const control of controls) {
dirtyState.delete(control);
control.classList.remove('user-invalid-fallback');
control.classList.remove('user-valid-fallback');
control.removeAttribute('aria-invalid');
}
return;
}
if (!input.checkValidity) return;
if (event.type === 'input' || event.type === 'change') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasInteracted = true;
dirtyState.set(input, state);
if (state.hasBlurred) {
updateState(input);
}
} else if (event.type === 'blur') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasBlurred = true;
dirtyState.set(input, state);
if (state.hasInteracted) {
updateState(input);
}
}
};
const init = (root = document) => {
if (CSS.supports('selector(:user-invalid)')) return;
root.addEventListener('blur', handleEvent, true); // Capture phase
root.addEventListener('input', handleEvent);
root.addEventListener('change', handleEvent);
root.addEventListener('reset', handleEvent, true); // Capture resets
};
return { init };
})();
// Initialize for a specific form
const form = document.querySelector('#demo-form');
UserInvalidFallback.init(form);
שיקולים נוספים
- כוכביות: עדיין מומלץ לציין שדות חובה באופן חזותי (למשל, עם כוכבית
*) בתווית, כדי שהמשתמשים ידעו למה לצפות לפני שהם יוצרים אינטראקציה. - כפתורי שליחה: בניגוד לכפתורי
disabled, כפתור השליחה צריך להיות מופעל. אם המשתמש לוחץ עליו, הדפדפן יפעיל באופן אוטומטי את:user-invalidבכל שדות החובה הריקים ויתמקד בשדה הראשון. זה מצוין לנגישות ולחוויית המשתמש. - נגישות: רכיב
:user-invalidמקורי לא מסתנכרן באופן אוטומטי עם מאפייני ARIA. כדי לסנכרן אתaria-invalidעם המצב החזותי, מוסיפים את קוד ה-JavaScript הבא:
// Sync aria-invalid with the CSS :user-invalid state
const syncAria = (el) => {
el.setAttribute?.('aria-invalid', el.matches(':user-invalid') ? 'true' : 'false');
};
// Update on blur (to show error) and input (to clear it)
document.addEventListener('blur', (e) => syncAria(e.target), true);
document.addEventListener('input', (e) => {
if (e.target.hasAttribute('aria-invalid')) syncAria(e.target);
});