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:
WorkRoot Agent
2026-04-13 12:18:24 +05:30
co-authored by Claude Opus 4.6
parent 9282a06a3c
commit 5a7ec6c9ed
+84 -21
View File
@@ -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'));