Fix form validation contrast for WCAG AA compliance on Contact page
Old green (#10b981) on light green bg had only 2.42:1 contrast and old red (#ef4444) on light red bg had 3.44:1 — both failing WCAG AA 4.5:1. Updated to dark green text/border (#155724) on #f0fff4 (8.39:1) and dark red text/border (#721c24) on #fff5f5 (10.29:1). Added ✓/✗ icons as non-color validation indicators, dark mode variants, and missing valid/invalid icons on phone, subject, and message fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9282a06a3c
commit
5a7ec6c9ed
+84
-21
@@ -196,11 +196,16 @@ const trustStats = [
|
||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-hidden="true">
|
||||
<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="name-error" data-error="name" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
@@ -231,11 +236,16 @@ const trustStats = [
|
||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||||
placeholder="john@example.com"
|
||||
/>
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-hidden="true">
|
||||
<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="email-error" data-error="email" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
@@ -267,6 +277,16 @@ const trustStats = [
|
||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
@@ -303,6 +323,16 @@ const trustStats = [
|
||||
<option value="support">Technical Support</option>
|
||||
<option value="other">Other / General Inquiry</option>
|
||||
</select>
|
||||
<div class="valid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Valid">
|
||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="invalid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Invalid">
|
||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none" aria-hidden="true">
|
||||
<svg class="w-4 h-4 text-secondary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
@@ -341,18 +371,30 @@ const trustStats = [
|
||||
</label>
|
||||
<span id="char-count" class="text-xs text-secondary-400" aria-hidden="true">0 / 5000</span>
|
||||
</div>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
minlength="10"
|
||||
maxlength="5000"
|
||||
rows="5"
|
||||
aria-required="true"
|
||||
aria-describedby="message-error message-hint"
|
||||
class="form-input w-full px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 resize-none"
|
||||
placeholder="Tell us about your project, goals, and timeline..."
|
||||
></textarea>
|
||||
<div class="relative">
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
minlength="10"
|
||||
maxlength="5000"
|
||||
rows="5"
|
||||
aria-required="true"
|
||||
aria-describedby="message-error message-hint"
|
||||
class="form-input w-full px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 resize-none"
|
||||
placeholder="Tell us about your project, goals, and timeline..."
|
||||
></textarea>
|
||||
<div class="valid-check absolute top-3 right-3 flex items-center hidden" aria-label="Valid">
|
||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="invalid-check absolute top-3 right-3 flex items-center hidden" aria-label="Invalid">
|
||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p id="message-hint" class="mt-1 text-xs text-secondary-400">Minimum 10 characters. Be as detailed as possible for a faster, more accurate response.</p>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="message-error" data-error="message" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
@@ -709,18 +751,34 @@ const trustStats = [
|
||||
}
|
||||
}
|
||||
|
||||
/* Form input states */
|
||||
/* Form input states — WCAG AA compliant (≥ 4.5:1 contrast) */
|
||||
.form-input.is-valid {
|
||||
border-color: #10b981;
|
||||
background-color: #f0fdf4;
|
||||
border-color: #155724;
|
||||
background-color: #f0fff4;
|
||||
color: #155724;
|
||||
}
|
||||
.form-input.is-invalid {
|
||||
border-color: #ef4444;
|
||||
background-color: #fef2f2;
|
||||
border-color: #721c24;
|
||||
background-color: #fff5f5;
|
||||
color: #721c24;
|
||||
}
|
||||
/* Dark mode validation states */
|
||||
:global(.dark) .form-input.is-valid {
|
||||
border-color: #48bb78;
|
||||
background-color: rgba(72, 187, 120, 0.1);
|
||||
color: #c6f6d5;
|
||||
}
|
||||
:global(.dark) .form-input.is-invalid {
|
||||
border-color: #fc8181;
|
||||
background-color: rgba(252, 129, 129, 0.1);
|
||||
color: #fed7d7;
|
||||
}
|
||||
.form-input.is-valid ~ .valid-check {
|
||||
display: flex;
|
||||
}
|
||||
.form-input.is-invalid ~ .invalid-check {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Budget option - checked state via JS since has-[:checked] may not work in all contexts */
|
||||
.budget-option.selected {
|
||||
@@ -857,6 +915,7 @@ const trustStats = [
|
||||
const errorEl = document.querySelector(`[data-error="${fieldName}"]`);
|
||||
const inputEl = document.getElementById(fieldName) as HTMLElement | null;
|
||||
const validCheck = inputEl?.parentElement?.querySelector('.valid-check') as HTMLElement | null;
|
||||
const invalidCheck = inputEl?.parentElement?.querySelector('.invalid-check') as HTMLElement | null;
|
||||
|
||||
if (errorEl) errorEl.classList.toggle('hidden', isValid !== false);
|
||||
|
||||
@@ -867,6 +926,9 @@ const trustStats = [
|
||||
if (validCheck) {
|
||||
validCheck.classList.toggle('hidden', isValid !== true);
|
||||
}
|
||||
if (invalidCheck) {
|
||||
invalidCheck.classList.toggle('hidden', isValid !== false);
|
||||
}
|
||||
}
|
||||
|
||||
function validateField(field: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): boolean {
|
||||
@@ -1017,6 +1079,7 @@ const trustStats = [
|
||||
(input as HTMLElement).classList.remove('is-valid', 'is-invalid');
|
||||
});
|
||||
form.querySelectorAll('.valid-check').forEach((el) => el.classList.add('hidden'));
|
||||
form.querySelectorAll('.invalid-check').forEach((el) => el.classList.add('hidden'));
|
||||
form.querySelectorAll('[data-error]').forEach((el) => el.classList.add('hidden'));
|
||||
if (charCount) charCount.textContent = '0 / 5000';
|
||||
budgetOptions.forEach((o) => o.classList.remove('selected'));
|
||||
|
||||
Reference in New Issue
Block a user