Color is not a reliable communication channel. Roughly 8% of men have color vision deficiency, making red/green-only slot states indistinguishable. This violates WCAG 2.2 SC 1.4.1 (Use of Color) directly, and SC 1.4.3 (Contrast Minimum) compounds the problem when low-contrast grays are used for past/blocked states. The legal exposure under ADA Title III is real for booking platforms serving the public. Beyond compliance, a color-blind user who cannot distinguish booked from available slots will either book into a conflict or abandon the flow — both outcomes cost revenue.
High because color-only slot states exclude approximately 8% of male users from reliably distinguishing bookable slots, violating WCAG 2.2 SC 1.4.1 and creating legal exposure under ADA Title III.
Pair every slot state color with an independent non-color indicator — an icon, a text label, or a pattern — so the state is decodable without color perception. Map all four states explicitly.
// src/components/CalendarSlot.tsx
const STATE = {
available: { bg: 'bg-green-100', icon: '✓', label: 'Available' },
booked: { bg: 'bg-red-100', icon: '✕', label: 'Booked' },
blocked: { bg: 'bg-gray-200', icon: '⊘', label: 'Blocked' },
past: { bg: 'bg-gray-100', icon: '◄', label: 'Past' },
} as const
export function CalendarSlot({ status, time }: { status: keyof typeof STATE; time: string }) {
const s = STATE[status]
return (
<div className={`${s.bg} p-2 rounded flex items-center gap-1`} aria-label={`${s.label}: ${time}`}>
<span aria-hidden="true">{s.icon}</span>
<span className="text-xs font-medium">{time}</span>
</div>
)
}
Verify contrast ratios for all four background/text combinations using the WCAG contrast checker — aim for ≥4.5:1 for normal text.
ID: booking-calendar-availability.calendar-display.visual-distinction
Severity: high
What to look for: Enumerate all slot states rendered in the calendar component. For each state (available, booked, blocked, past), count the number of independent visual cues (color, icon, text label, pattern, tooltip). Inspect the calendar slot component for CSS classes, inline styles, and rendered text/icons per state. Examine files matching src/components/*Slot*, src/components/*Calendar*, src/components/*slot*.
Pass criteria: Count all slot states rendered in the calendar. Each of the 4 states (available, booked, blocked, past) must use at least 2 independent visual cues (e.g., color + icon, color + text label, pattern + tooltip). No more than 0 states may rely on color alone. Report: "X of 4 slot states use 2+ independent visual cues."
Fail criteria: States are distinguished by color alone, or no visual distinction between booked and blocked slots.
Do NOT pass when: Booked and blocked slots share the same styling (even if both have icons) — these are distinct states that require distinct visual treatment.
Skip (N/A) when: Calendar does not show slot availability states (e.g., text list instead of grid, or single-state display).
Detail on fail: Example: "Available slots are green, booked are red. No icons or text labels. A color-blind user cannot tell which slots are available."
Cross-reference: Check unavailable-feedback — visual distinction and click feedback work together for unavailable slots.
Cross-reference: Check keyboard-navigation — visual focus indicators must also be distinguishable alongside state colors.
Cross-reference: Check past-date-protection — past dates are one of the 4 states that need distinct visual treatment.
Remediation: Use color + additional indicators:
interface SlotProps {
status: 'available' | 'booked' | 'blocked' | 'past'
time: string
}
export function CalendarSlot({ status, time }: SlotProps) {
const statusConfig = {
available: {
bg: 'bg-green-100',
text: 'text-green-900',
icon: '✓',
label: 'Available',
},
booked: {
bg: 'bg-red-100',
text: 'text-red-900',
icon: '✕',
label: 'Booked',
},
blocked: {
bg: 'bg-gray-200',
text: 'text-gray-700',
icon: '⊘',
label: 'Blocked',
},
past: {
bg: 'bg-gray-100',
text: 'text-gray-500',
icon: '◄',
label: 'Past',
},
}
const config = statusConfig[status]
return (
<div
className={`${config.bg} ${config.text} p-2 rounded border`}
title={config.label}
>
<span>{config.icon}</span>
<span className="ml-1">{time}</span>
</div>
)
}