diff --git a/index.html b/index.html index 4e1d5eb..6fea1b6 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + - Accessibility Done Wrong +
diff --git a/public/audio_1-2-1.mp3 b/public/audio_1-2-1.mp3 new file mode 100644 index 0000000..c72ea96 Binary files /dev/null and b/public/audio_1-2-1.mp3 differ diff --git a/public/audio_1-4-2.mp3 b/public/audio_1-4-2.mp3 new file mode 100644 index 0000000..f10b62f Binary files /dev/null and b/public/audio_1-4-2.mp3 differ diff --git a/public/image_1-4-5.png b/public/image_1-4-5.png new file mode 100644 index 0000000..301c01a Binary files /dev/null and b/public/image_1-4-5.png differ diff --git a/public/video_1-2-1.mp4 b/public/video_1-2-1.mp4 new file mode 100644 index 0000000..5c0fc4f Binary files /dev/null and b/public/video_1-2-1.mp4 differ diff --git a/public/video_1-2-2.mp4 b/public/video_1-2-2.mp4 new file mode 100644 index 0000000..29effc6 Binary files /dev/null and b/public/video_1-2-2.mp4 differ diff --git a/public/video_1-2-3.mp4 b/public/video_1-2-3.mp4 new file mode 100644 index 0000000..0a4dd5b Binary files /dev/null and b/public/video_1-2-3.mp4 differ diff --git a/public/video_1-2-4.mp4 b/public/video_1-2-4.mp4 new file mode 100644 index 0000000..60a718d Binary files /dev/null and b/public/video_1-2-4.mp4 differ diff --git a/public/video_1-2-5.mp4 b/public/video_1-2-5.mp4 new file mode 100644 index 0000000..45c8145 Binary files /dev/null and b/public/video_1-2-5.mp4 differ diff --git a/src/App.module.css b/src/App.module.css index 93850c3..eae56f5 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -12,10 +12,18 @@ h1 { section { display: grid; - gap: 1rem; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; +} + +@media screen and (max-width: 600px) { + section { + grid-template-columns: 1fr; + } } section > header { + grid-column: 1 / -1; display: grid; grid-template-columns: 1fr auto; align-items: center; @@ -31,3 +39,659 @@ section > header > p { font-size: 1.25rem; /* 20px */ line-height: 1.6; } + +ul { + margin: 0; + padding-left: 1.25em; +} + +nav > ul { + display: flex; + gap: 0.5rem; + padding: 0; + list-style-type: none; +} + +button[role='tab'] { + border-bottom: 2px solid transparent; +} + +button[role='tab'][aria-selected='true'] { + border-color: currentColor; +} + +button[role='tab'], +button[role='tab']:not([disabled]):hover, +button[role='tab']:not([disabled]):focus-visible, +button[role='tab']:not([disabled]):active { + background: none; +} + +button[role='tab']:not([disabled]):not([aria-selected='true']):hover::before, +button[role='tab']:not([disabled]):focus-visible::before, +button[role='tab']:not([disabled]):not([aria-selected='true']):active::before { + content: ''; + position: absolute; + inset: 0; + background-color: currentColor; + opacity: 0.1; +} + +button[role='tab']:not([disabled]):not([aria-selected='true']):active::before { + opacity: 0.05; +} + +img, +audio, +video { + width: 100%; +} + +.transcript { + color: var(--ubilabs-font-secondary-color); +} + +.transcript h3 { + font-size: 1rem; /* 16px */ +} + +.transcript p { + font-size: 0.875rem; /* 14px */ +} + +.infoAndRelationships { + display: flex; + flex-direction: column; + gap: 0.25rem; + align-items: start; +} + +.infoAndRelationships p { + margin: 0; +} + +.infoAndRelationships input { + margin-bottom: 0.75rem; +} + +.meaningfulSequence { + display: grid; +} + +.sensoryCharacteristics button:not(:last-child) { + margin-right: 0.5rem; +} + +.landscapeWarning { + color: var(--ubilabs-red); +} + +@media screen and (orientation: portrait) { + .orientationContent { + display: none; + } +} + +@media screen and (orientation: landscape) { + .landscapeWarning { + display: none; + } +} + +.identityInputPurpose { + display: flex; + flex-direction: column; + gap: 0.25rem; + align-items: start; + + button { + margin-top: 0.5rem; + } +} + +.useOfColor tr > *:first-child { + padding-right: 1rem; + text-align: start; +} + +.useOfColor tr > *:last-child { + text-align: center; +} + +.audioControlHint { + color: var(--ubilabs-font-secondary-color); +} + +.contrastMinimum { + display: flex; + padding: 1rem; + color: var(--ubilabs-gray-500); + background-color: #787878; +} + +.contrastMinimum p { + margin: auto; + text-align: center; +} + +.resizeTextContainer { + height: 50px; + overflow: hidden; +} + +.reflow { + width: 100%; + height: 80px; + padding: 1rem; + overflow: auto; + border: 1px solid var(--ubilabs-gray-500); +} + +.reflow p { + width: 320px; +} + +.nonTextContrast { + display: flex; + flex-direction: column; + align-items: start; + gap: 1rem; +} + +.lowContrastButton { + gap: 0.5rem; + background-color: var(--ubilabs-gray-500); + color: var(--ubilabs-anthracite); +} + +.inputWrapper input { + border: 1px solid var(--ubilabs-gray-800); + padding: 0.5rem; + background-color: var(--ubilabs-anthracite); + color: var(--ubilabs-white); +} + +@media (prefers-color-scheme: light) { + .inputWrapper input { + border: 1px solid #eaeaea; + } +} + +.textSpacingContainer { + width: 16rem; + height: 11.524375em; + margin-bottom: 1rem; + padding: 1rem; + overflow: hidden; + border: 1px solid var(--ubilabs-gray-500); +} + +.customTextSpacing p { + margin-bottom: 2em; + line-height: 1.5em; + letter-spacing: 0.12em; + word-spacing: 0.16em; +} + +.contentOnHoverOrFocus { + position: relative; +} + +.contentOnHoverOrFocus .tooltip { + max-width: 14rem; + margin-top: 0.5rem; + padding: 1rem; + background: var(--ubilabs-gray-800); + color: var(--ubilabs-font-color); + font-size: 0.875rem; +} + +@media (prefers-color-scheme: light) { + .contentOnHoverOrFocus .tooltip { + background: var(--ubilabs-gray-200); + } +} + +.keyboardTrap { + border: 2px solid var(--ubilabs-gray-500); + + > div { + padding: 1rem; + + &:not(:last-child) { + border-bottom: 1px solid var(--ubilabs-gray-500); + } + } +} + +.timingAdjustable { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.timingAdjustable form { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + gap: 1rem 0.5rem; +} + +.timingAdjustable form button { + grid-column: 1 / -1; +} + +.timingAdjustable .expiredMessage { + grid-column: 1 / -1; + color: var(--ubilabs-red); + font-size: 0.875rem; +} + +.pauseStopHide { + overflow: hidden; +} + +.scrollingBanner { + width: 15rem; + padding: 0.5rem; + overflow: hidden; + background-color: var(--ubilabs-red-800); + color: var(--ubilabs-white); + font-weight: 500; + text-transform: uppercase; + white-space: nowrap; +} + +.scrollingBanner p { + display: inline-block; + margin: 0; + animation: scroll-left 10s linear infinite; +} + +@keyframes scroll-left { + 0% { + transform: translateX(15rem); + } + 100% { + transform: translateX(-100%); + } +} + +.flashingBox { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background-color: var(--ubilabs-red-800); + color: var(--ubilabs-white); + font-size: 1.5rem; /* 24px */ + font-weight: bold; + letter-spacing: 0.05em; + text-transform: uppercase; + animation: flashWarning 0.25s infinite; /* 4 flashes per second */ +} + +@keyframes flashWarning { + 0%, + 100% { + background-color: var(--ubilabs-red-800); + } + 50% { + background-color: var(--ubilabs-yellow-900); + } +} + +.pageTitled { + color: var(--ubilabs-font-secondary-color); +} + +.focusOrder { + display: flex; + flex-direction: column; + align-items: start; + gap: 1rem; +} + +.multipleWays nav[role='tablist'] { + margin-bottom: 1rem; +} + +.headingsAndLabels { + display: flex; + flex-direction: column; + align-items: start; + gap: 0.5rem; +} + +.headingsAndLabels label { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.noFocusVisible { + display: flex; + align-self: start; + align-items: center; + gap: 1rem; +} + +.noFocusVisible button:not([disabled]):focus-visible { + outline: none; + box-shadow: none; + background-color: #3c55dc; +} + +.noFocusVisible a:focus-visible { + outline: none; + color: var(--ubilabs-font-color); +} + +.focusNotObscured { + position: relative; + height: 9rem; + overflow: auto; + outline: 1px solid var(--ubilabs-gray-500); +} + +.focusNotObscured .header { + z-index: 2; + position: sticky; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 3.5rem; + margin-bottom: -3.5rem; + padding: 0 1rem; + background: var(--ubilabs-gray-700); + color: var(--ubilabs-white); + pointer-events: none; /* so it doesn't block clicks for this demo */ +} + +.focusNotObscured .content { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; +} + +.pointerGestures { + position: relative; +} + +.pointerGestures .secretContent { + width: 13rem; + padding: 1rem; + background-color: var(--ubilabs-yellow-900); + color: var(--ubilabs-white); + font-weight: 500; + text-transform: uppercase; + text-align: center; +} + +.pointerGestures .draggable { + position: absolute; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 13rem; + padding: 1rem; + background: var(--ubilabs-anthracite); + color: var(--ubilabs-white); + border: 1px solid var(--ubilabs-white); + user-select: none; + touch-action: none; + cursor: grab; + text-align: center; +} + +.pointerGestures .draggable::before { + content: '←'; +} + +.pointerGestures .draggable::after { + content: '→'; +} + +.labelInName form { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.draggingMovements { + position: relative; +} + +.draggingMovements .dropZones { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.draggingMovements .dropZones > div { + display: flex; + align-items: center; + justify-content: center; + padding: 50% 0; + border: 1px solid var(--ubilabs-gray-500); + color: var(--ubilabs-font-secondary-color); +} + +.draggingMovements .dropZones > div > span { + position: absolute; + text-transform: uppercase; +} + +.draggingMovements .slider { + position: absolute; + top: 0; + left: 0; + width: 3.5rem; + height: 3.5rem; + margin: 1rem; + background-color: var(--ubilabs-blue); + user-select: none; + touch-action: none; + cursor: grab; +} + +.draggingMovements p { + margin-top: 1rem; +} + +.targetSize { + display: flex; + gap: 0.25rem; +} + +.targetSize button { + min-width: 0; + min-height: 0; + height: 16px; + padding: 0.25em; + font-size: 0.675rem; +} + +.languageOfPage { + color: var(--ubilabs-font-secondary-color); +} + +.inconsistentNavigation { + display: grid; + grid-template-columns: repeat(2, auto); + grid-template-rows: auto 1fr; + align-items: start; + justify-content: start; + gap: 1rem 0.5rem; +} + +.inconsistentNavigation .page { + grid-column: 1 / -1; + padding: 1rem; + border: 1px solid var(--ubilabs-gray-500); +} + +.consistentIdentification { + display: grid; + grid-template-columns: repeat(2, 1fr); + align-items: start; + gap: 1rem; +} + +.consistentIdentification .product { + padding: 1rem; + border: 1px solid var(--ubilabs-gray-500); +} + +.consistentIdentification dialog { + border: 1px solid var(--ubilabs-gray-500); +} + +.consistentIdentification dialog::backdrop { + background-color: var(--ubilabs-anthracite); + opacity: 0.8; +} + +.consistentHelp { + display: grid; + gap: 1rem; +} + +.consistentHelp .page { + display: grid; + grid-template-columns: 1fr auto; + padding: 1rem; + border: 1px solid var(--ubilabs-gray-500); +} + +.consistentHelp .page > p { + grid-column: 1 / -1; + grid-row: 2; +} + +.consistentHelp .helpLinkTopRight { + grid-column: 2; + grid-row: 1; +} + +.consistentHelp .helpLinkBottomLeft { + grid-column: 1; + grid-row: 3; +} + +.errorIdentification { + display: flex; + flex-direction: column; + align-items: start; + gap: 0.25rem; +} + +.errorIdentification button { + margin-top: 0.75rem; +} + +.labelsOrInstruction { + display: flex; + flex-direction: column; + align-items: start; + gap: 0.25rem; +} + +.labelsOrInstruction .visuallyHidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + +.labelsOrInstruction button { + margin-top: 0.75rem; +} + +.errorSuggestion { + display: flex; + flex-direction: column; + align-items: start; + gap: 0.25rem; +} + +.errorSuggestion [role='alert'] { + color: var(--ubilabs-red); + font-size: 0.875rem; +} + +.errorSuggestion button { + margin-top: 0.75rem; +} + +.errorPrevention { + display: flex; + flex-direction: column; + align-items: start; + gap: 0.25rem; +} + +.errorPrevention button { + margin-top: 0.75rem; +} + +.redundantEntry form { + display: flex; + flex-direction: column; + align-items: start; + gap: 0.25rem; +} + +.redundantEntry .buttons { + display: flex; + gap: 0.5rem; +} + +.redundantEntry button { + margin-top: 0.75rem; +} + +.nameRoleValue { + display: flex; + gap: 0.25rem; +} + +.nameRoleValue .checkbox { + display: inline-block; + width: 1.5rem; + height: 1.5rem; + border-radius: 2px; + border: 1px solid var(--ubilabs-gray-500); +} + +.nameRoleValue .checkbox:hover { + border-color: var(--ubilabs-font-color); +} + +.statusMessages form { + display: flex; + flex-direction: column; + align-items: start; + gap: 0.25rem; +} + +.statusMessages button { + margin-top: 0.75rem; +} + +.statusMessages .message { + margin-top: 0.5rem; + color: var(--ubilabs-green); + font-size: 0.875rem; +} diff --git a/src/App.tsx b/src/App.tsx index 50fa1e7..12a6c08 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,314 @@ -import style from './App.module.css'; +import { + useEffect, + useRef, + useState, + type MouseEvent, + type KeyboardEvent as ReactKeyboardEvent, + type TouchEvent +} from 'react'; +import styles from './App.module.css'; + +const MULTIPLE_WAYS_TABS = ['Tab 1', 'Tab 2', 'Tab 3']; function App() { + /* 1.4.12 Text Spacing */ + const [useCustomTextSpacing, setUseCustomSpacing] = useState(false); + + const toggleCustomTextSpacing = () => { + setUseCustomSpacing(prevState => !prevState); + }; + + /* 1.4.13 Content on Hover or Focus */ + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + + const showTooltip = () => { + setIsTooltipVisible(true); + }; + const hideTooltip = () => { + setIsTooltipVisible(false); + }; + + /* 2.1.4 Character Key Shortcuts */ + useEffect(() => { + // Delete draft if `d` key is pressed + const deleteDraft = (event: KeyboardEvent) => { + if (event.key === 'd') { + alert('Draft deleted!'); + } + }; + + document.addEventListener('keydown', deleteDraft); + + return () => { + document.removeEventListener('keydown', deleteDraft); + }; + }, []); + + /* 2.2.1 Timing Adjustable */ + const [isFormExpired, setIsFormExpired] = useState(false); + + useEffect(() => { + // Auto-expire form after 10 seconds + const timeout = setTimeout(() => { + setIsFormExpired(true); + }, 10000); + + return () => { + clearTimeout(timeout); + }; + }, []); + + /* 2.4.5 Multiple Ways */ + const [activeTab, setActiveTab] = useState(MULTIPLE_WAYS_TABS[0]); + const tabRefs = useRef>([]); + + const focusTab = (index: number) => { + const tab = tabRefs.current[index]; + + if (tab) { + tab.focus(); + setActiveTab(`Tab ${index + 1}`); + } + }; + + const handleTabKeyDown = ( + event: ReactKeyboardEvent, + index: number + ) => { + const lastIndex = tabRefs.current.length - 1; + + switch (event.key) { + case 'ArrowRight': + event.preventDefault(); + focusTab(index === lastIndex ? 0 : index + 1); + break; + + case 'ArrowLeft': + event.preventDefault(); + focusTab(index === 0 ? lastIndex : index - 1); + break; + + case 'Home': + event.preventDefault(); + focusTab(0); + break; + + case 'End': + event.preventDefault(); + focusTab(lastIndex); + break; + + default: + break; + } + }; + + /* 2.5.1 Pointer Gesture */ + const pointerGestureDraggableRef = useRef(null); + const pointerGestureStartX = useRef(0); + const [pointerGestureIsDragging, setPointerGestureIsDragging] = + useState(false); + + const pointerGestureStartDrag = ( + event: MouseEvent | TouchEvent + ) => { + setPointerGestureIsDragging(true); + pointerGestureStartX.current = + 'touches' in event ? event.touches[0].clientX : event.clientX; + }; + + const pointerGestureOnDrag = ( + event: MouseEvent | TouchEvent + ) => { + if (!pointerGestureIsDragging || !pointerGestureDraggableRef.current) { + return; + } + + const currentX = + 'touches' in event ? event.touches[0].clientX : event.clientX; + const delta = currentX - pointerGestureStartX.current; + pointerGestureDraggableRef.current.style.transform = `translateX(${delta}px)`; + }; + + const pointerGestureEndDrag = () => { + setPointerGestureIsDragging(false); + + if (pointerGestureDraggableRef.current) { + pointerGestureDraggableRef.current.style.transform = 'translateX(0)'; + } + }; + + /* 2.5.4 Motion Actuation */ + const motionContentRef = useRef(null); + + useEffect(() => { + let isFirstEvent = true; + + // Refresh content on device tilt + const handleOrientationChange = (event: DeviceOrientationEvent) => { + if (isFirstEvent) { + isFirstEvent = false; + return; + } + + if (event.beta !== null && motionContentRef.current) { + motionContentRef.current.innerText = `Content refreshed at ${new Date().toLocaleTimeString()}`; + } + }; + + window.addEventListener('deviceorientation', handleOrientationChange); + + return () => + window.removeEventListener('deviceorientation', handleOrientationChange); + }, []); + + /* 2.5.7 Dragging Movements */ + const draggingMovementsDraggableRef = useRef(null); + const draggingMovementsDropZone1Ref = useRef(null); + const draggingMovementsDropZone2Ref = useRef(null); + const draggingMovementsStartX = useRef(0); + const draggingMovementsStartY = useRef(0); + const draggingMovementsDeltaX = useRef(0); + const draggingMovementsDeltaY = useRef(0); + const [draggingMovementsIsDragging, setDraggingMovementsIsDragging] = + useState(false); + + const draggingMovementsStartDrag = ( + event: MouseEvent | TouchEvent + ) => { + setDraggingMovementsIsDragging(true); + draggingMovementsStartX.current = + 'touches' in event ? event.touches[0].clientX : event.clientX; + draggingMovementsStartY.current = + 'touches' in event ? event.touches[0].clientY : event.clientY; + }; + + const draggingMovementsOnDrag = ( + event: MouseEvent | TouchEvent + ) => { + if ( + !draggingMovementsIsDragging || + !draggingMovementsDraggableRef.current + ) { + return; + } + + const currentX = + 'touches' in event ? event.touches[0].clientX : event.clientX; + const currentY = + 'touches' in event ? event.touches[0].clientY : event.clientY; + + draggingMovementsDeltaX.current = + currentX - draggingMovementsStartX.current; + draggingMovementsDeltaY.current = + currentY - draggingMovementsStartY.current; + + draggingMovementsDraggableRef.current.style.transform = `translate(${draggingMovementsDeltaX.current}px, ${draggingMovementsDeltaY.current}px)`; + }; + + const draggingMovementsEndDrag = () => { + setDraggingMovementsIsDragging(false); + + if (!draggingMovementsDraggableRef.current) { + return; + } + + draggingMovementsDraggableRef.current.style.transform = 'translate(0, 0)'; + + const originalDraggableRect = + draggingMovementsDraggableRef.current.getBoundingClientRect(); + const draggableRect = { + top: originalDraggableRect.top + draggingMovementsDeltaY.current, + bottom: originalDraggableRect.bottom + draggingMovementsDeltaY.current, + left: originalDraggableRect.left + draggingMovementsDeltaX.current, + right: originalDraggableRect.right + draggingMovementsDeltaX.current + }; + + const dropZone1Rect = + draggingMovementsDropZone1Ref.current?.getBoundingClientRect(); + const dropZone2Rect = + draggingMovementsDropZone2Ref.current?.getBoundingClientRect(); + + if ( + dropZone1Rect && + draggableRect.right >= dropZone1Rect.left && + draggableRect.left <= dropZone1Rect.right && + draggableRect.bottom >= dropZone1Rect.top && + draggableRect.top <= dropZone1Rect.bottom + ) { + draggingMovementsDraggableRef.current.style.left = '0'; + } else if ( + dropZone2Rect && + draggableRect.right >= dropZone2Rect.left && + draggableRect.left <= dropZone2Rect.right && + draggableRect.bottom >= dropZone2Rect.top && + draggableRect.top <= dropZone2Rect.bottom + ) { + draggingMovementsDraggableRef.current.style.left = 'calc(50% + 0.5rem)'; + } + }; + + /* 3.2.1 On Focus */ + const isFocusAlertOpen = useRef(false); + + const onFocus = () => { + if (!isFocusAlertOpen.current) { + alert('An alert opens automatically on focus!'); + isFocusAlertOpen.current = true; + } else { + isFocusAlertOpen.current = false; + } + }; + + /* 3.2.3 Consistent Navigation */ + const [consistentNavigationPage, setConsistentNavigationPage] = useState< + 'home' | 'other' + >('home'); + + /* 3.2.4 Consistent Identification */ + const consistentIdentificationDialogRef = useRef( + null + ); + const [ + consistentIdentificationModalContent, + setConsistentIdentificationModalContent + ] = useState(null); + + /* 3.3.3 Error Suggestion */ + const [usernameValidationError, setUsernameValidationError] = useState< + string | null + >(null); + + const validateUsername = (username: string) => { + if (username.length === 0) { + setUsernameValidationError('Username is required.'); + return false; + } else if (username.length < 5) { + setUsernameValidationError('Username is invalid.'); + return false; + } else { + setUsernameValidationError(null); + return true; + } + }; + + /* 3.3.7 Redundant Entry */ + const [redundantEntryStep, setRedundantEntryStep] = useState(1); + + const handleNextRedundantEntryStep = () => { + setRedundantEntryStep(prevStep => Math.max(2, prevStep + 1)); + }; + + const handlePreviousRedundantEntryStep = () => { + setRedundantEntryStep(prevStep => Math.max(1, prevStep - 1)); + }; + + /* 4.1.2 Name, Role, Value */ + const [isChecked, setIsChecked] = useState(false); + + /* 4.1.3 Status Messages */ + const [showStatusMessage, setShowStatusMessage] = useState(false); + return ( <>

Accessibility Done Wrong

@@ -8,436 +316,1478 @@ function App() {

1.1.1 Non-Text Content

- Level A + Level A

Provide text alternatives for non-text content that serves the same purpose.

- {/* TODO: Add example content here that violates this accessibility rule */} + + +
+

Why this violates the rule

+
    +
  • + The <img> tag has no alt attribute at all. +
  • +
+

1.2.1 Audio-Only and Video-Only (Prerecorded)

- Level A + Level A

Provide an alternative to video-only and audio-only content.

- {/* TODO: Add example content here that violates this accessibility rule */} + + +
+

Why this violates the rule

+
    +
  • There's no transcript or text alternative.
  • +
  • + Important info (time, instructions) is only available in audio. +
  • +
+
+ + + +
+

Why this violates the rule

+
    +
  • There's no audio description or text alternative.
  • +
  • + Important info (how to cut the corn off the cob) is only available + in video. +
  • +
+

1.2.2 Captions (Prerecorded)

- Level A + Level A

Provide captions for videos with audio.

- {/* TODO: Add example content here that violates this accessibility rule */} + + +
+

Why this violates the rule

+
    +
  • + The video contains spoken dialogue, but no captions are provided. +
  • +
  • + Deaf or hard-of-hearing users cannot access the spoken content. +
  • +
  • + Captions are required to present both dialogue and relevant sound + effects. +
  • +
+

1.2.3 Audio Description or Media Alternative (Prerecorded)

- Level A + Level A

- Provide audio description or text transcript for videos with sound. + Provide audio description or a text alternative for videos with + sound and meaningful visual content.

- {/* TODO: Add example content here that violates this accessibility rule */} + + +
+

Why this violates the rule

+
    +
  • + The video includes important visual information that is not + described in the audio. +
  • +
  • + Users who are blind or have low vision cannot access this content + fully. +
  • +
+

1.2.4 Captions (Live)

- Level AA + Level AA

Add captions to live videos.

- {/* TODO: Add example content here that violates this accessibility rule */} + + +
+

Why this violates the rule

+
    +
  • The video simulates a live event with spoken content.
  • +
  • No real-time captions are provided.
  • +
  • + Deaf or hard-of-hearing users cannot follow the spoken dialogue as + it happens. +
  • +
+
-

1.2.5 Audio Description (Pre-Recorded)

- Level AA -

Provide audio descriptions for pre-recorded videos.

+

1.2.5 Audio Description (Prerecorded)

+ Level AA +

+ Provide audio descriptions for videos with sound and important + visual content. +

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + +
+

Transcript

+

+ [00:00] SpongeBob SquarePants jumps around his living room, picks + up his name tag, and joyfully sings, “It’s Monday. It’s Monday. + It’s Monday!” +

+

+ [00:05] He leaves his pineapple house and twirls down the street, + still singing, “Thank gosh it’s Mondaaaay!!!” while dancing + enthusiastically. +

+
+
+ +
+

Why this violates the rule

+
    +
  • + The video includes important visual details that are not described + in the audio. +
  • +
  • + No audio description is included, either as part of the main audio + track or as an alternate track. +
  • +
  • + A transcript is not sufficient at this level — blind or low-vision + users miss visual context in real time. +
  • +
+

1.3.1 Info and Relationships

- Level A + Level A

Content, structure and relationships can be programmatically determined.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+

Name:

+ + +

Email:

+ + +

Subscribe to newsletter:

+ +
+ +
+

Why this violates the rule

+
    +
  • + Visual labels are presented using <p> tags, not + with proper <label> elements. +
  • +
  • + Screen reader users will not know what each form input is for, + because the semantic relationship between the label and the input + is missing. +
  • +
  • + Visual proximity alone is not enough — labels must be + programmatically associated using the{' '} + <label for=""> attribute or by wrapping inputs + in <label> elements. +
  • +
+

1.3.2 Meaningful Sequence

- Level A + Level A

Present content in a meaningful order.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+

Step 1

+

Turn on the oven.

+
+ +
+

Step 3

+

Bake for 30 minutes.

+
+ +
+

Step 2

+

Mix the ingredients.

+
+
+ +
+

Why this violates the rule

+
    +
  • + Visually, the steps appear top-to-bottom, from step 1 to step 3, + but in the DOM, the order is: step 1 → step 3 → step 2, which may + not match the intended sequence. +
  • +
  • + The sequence is not meaningful for non-visual users — they hear + the steps out of logical order. +
  • +
+

1.3.3 Sensory Characteristics

- Level A + Level A

Instructions don't rely solely on sensory characteristics.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+

+ To proceed, click the green button below. +

+ + +
+ +
+

Why this violates the rule

+
    +
  • + The instruction relies only on the color of the button ("green + button"). +
  • +
  • + Users cannot determine which button performs the correct action + without visual cues. +
  • +
+

1.3.4 Orientation

- Level AA + Level AA

Your website adapts to portrait and landscape views.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+

+ Please rotate your device to landscape mode to continue. +

+ +
+

+ This is the main application content that only shows in landscape. +

+
+
+ +
+

Why this violates the rule

+
    +
  • + The app forces users to switch to landscape orientation to access + content. +
  • +
  • + Users who have orientation lock enabled (due to motor impairments + or personal preference) cannot proceed. +
  • +
  • + There is no essential reason to limit access to landscape — the + content would work just fine in portrait. +
  • +
+

1.3.5 Identify Input Purpose

- Level AA + Level AA

The purpose of input fields must be programmatically determinable.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + + + + + + + + + +
+ +
+

Why this violates the rule

+
    +
  • + The fields collect user-specific data, but there are no + autocomplete attributes. +
  • +
  • + Assistive tech and browsers cannot identify the purpose of the + inputs. +
  • +
  • + It misses an opportunity for autofill, which helps all users, + especially those with memory or motor challenges. +
  • +
+

1.4.1 Use of Color

- Level A -

Don't use presentation that relies solely on colour.

+ Level A +

Don't use presentation that relies solely on color.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + + + + + + + + + + + + + + + + +
TaskStatus
Submit report
Review feedback
+
+ +
+

Why this violates the rule

+
    +
  • The status is only communicated using color.
  • +
  • + Users who are color-blind may not distinguish red from green. +
  • +
  • + There are no icons, text labels, or other indicators to + communicate status. +
  • +
+

1.4.2 Audio Control

- Level A -

Don't play audio automatically.

+ Level A +

+ If audio plays automatically for more than 3 seconds, users must be + able to pause, stop, or adjust it. +

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + +

+ Your browser may block this audio element due to autoplay + restrictions of modern browsers. +

+
+ +
+

Why this violates the rule

+
    +
  • + The audio starts playing automatically as soon as the page loads + and plays for longer than 3 seconds, but there are no playback + controls — the user cannot pause, stop, or mute the sound. +
  • +
  • + This interferes with screen readers and focus, making the page + unusable for many users. +
  • +
+

1.4.3 Contrast Minimum

- Level AA + Level AA

Contrast ratio between text and background is at least 4.5:1.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+

This text has insufficient contrast with its background.

+
+ +
+

Why this violates the rule

+
    +
  • + The text color #a7a7a7 and background color{' '} + #787878 have a contrast ratio far below the 4.5:1 + minimum. +
  • +
  • + Users with low vision or color deficiencies will have difficulty + reading the text. +
  • +
+

1.4.4 Resize Text

- Level AA + Level AA

Text can be resized to 200% without loss of content or function.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ This text is stuck at 16px and won’t grow with user settings. +
+ +
+

Why this violates the rule

+
    +
  • + The text uses font-size: 16px, which is an absolute + unit. Users who increase their preferred font size in browser or + OS settings won't see any effect. +
  • +
  • + This prevents users with low vision or reading difficulties from + enlarging the text to a readable size. +
  • +
+
+ +
+ This is a long sentence that will be clipped when the text size + increases. +
+ +
+

Why this violates the rule

+
    +
  • + The container has a fixed height and overflow: hidden + , so it cannot grow to fit larger text. +
  • +
  • + When users increase text size (without zoom), the bottom portion + is clipped and unreadable. +
  • +
+

1.4.5 Images of Text

- Level AA + Level AA

Don't use images of text.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ I'm text in an image. +
+ +
+

Why this violates the rule

+
    +
  • + The text ("I'm text in an image.") is presented as part of an + image, rather than as real, selectable, scalable text. +
  • +
  • + Users cannot resize the text independently or adjust contrast and + colors using their assistive tech or browser settings. +
  • +
  • + While the image includes an alt attribute, it does + not offer the flexibility or accessibility of real text. +
  • +
  • + Decorative styles (fonts, colors, etc.) could be achieved with + HTML and CSS instead of an image. +
  • +
+

1.4.10 Reflow

- Level AA + Level AA

Content retains meaning and function without scrolling in two - dimensions. + dimensions for viewport widths between 320px and 1024px.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+

+ This paragraph is placed inside a fixed-size container with limited + width and height. When a user zooms in to 400% or views it on a + small screen, the text becomes too large to fit and requires both + horizontal and vertical scrolling to read. +

+
+ +
+

Why this violates the rule

+
    +
  • + The container has a fixed width and height, which prevents the + content from reflowing properly when zoomed. +
  • +
  • + At 400% zoom or on small screens (like mobile devices), users must + scroll in both directions — horizontally and vertically — to + access all the text. +
  • +
  • + This layout makes it harder for users with low vision or screen + magnification to access the information efficiently. +
  • +
+

1.4.11 Non-Text Contrast

- Level AA + Level AA

The contrast between user interface components, graphics and - adjacent colours is at least 3:1. + adjacent colors is at least 3:1.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+ +
+ + +
+ +
+

Why this violates the rule

+
    +
  • + The text input has a visible border only slightly different than + its background, making it hard for users with low vision to + perceive the input field. +
  • +
  • + The circular icon in the button has a light gray fill on a gray + background, resulting in a contrast ratio below the required 3:1. +
  • +
  • + Non-text elements that convey meaning or enable interaction (like + icons, focus indicators, and form borders) must meet the minimum + contrast to be perceivable by all users. +
  • +
+

1.4.12 Text Spacing

- Level AA + Level AA

Content and function retain meaning when users change elements of text spacing.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+

+ This text sits inside a fixed-size container with tight spacing. +

+

+ When users increase line, word, or letter spacing, it overflows or + gets cut off. +

+
+ + +
+ +
+

Why this violates the rule

+
    +
  • + The container uses fixed height and no wrapping, which causes the + text to be clipped or overlap when users override spacing via user + styles or browser extensions. +
  • +
  • + Users who increase spacing for better readability (especially + those with dyslexia or low vision) may lose access to content if + spacing causes layout breakage. +
  • +
  • + The content must remain usable and visible when spacing is + increased to the following minimums: +
      +
    • + Line height (line spacing): at least 1.5 times the font size +
    • +
    • + Spacing following paragraphs: at least 2 times the font size +
    • +
    • + Letter spacing (tracking): at least 0.12 times the font size +
    • +
    • Word spacing: at least 0.16 times the font size
    • +
    +
  • +
+

1.4.13 Content on Hover or Focus

- Level AA + Level AA

- When hover or focus triggers content to appear, it is dismissible, - hoverable and persistent. + When hover or focus triggers content (like tooltips or popovers) to + appear, it must be dismissible, hoverable and persistent.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + + {isTooltipVisible && ( +
+ This tooltip disappears when you move the mouse or when the button + loses focus. It can't be focused itself and can't be dismissed + with a key or button click. +
+ )} +
+ +
+

Why this violates the rule

+
    +
  • + The tooltip disappears immediately when the pointer moves off the + button — it's not hoverable. +
  • +
  • + Users cannot dismiss the tooltip manually e.g. by pressing{' '} + Esc or clicking a close button. +
  • +
  • + The tooltip can’t be focused with the keyboard and vanishes too + quickly, making it inaccessible for many users. +
  • +
+

2.1.1 Keyboard

- Level A + Level A

All functionality is accessible by keyboard with no specific timings.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
alert('Fake button was clicked!')} + role="button"> + Fake button + (no keyboard support) +
+
+ +
+

Why this violates the rule

+
    +
  • + The fake button can be activated by mouse click, but it is not + keyboard focusable. Keyboard users cannot access or operate the + fake button. +
  • +
  • + All interactive elements must be reachable and usable with + keyboard alone. +
  • +
+

2.1.2 No keyboard Trap

- Level A + Level A

Users can navigate to and from all content using a keyboard.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+ Menu item before the trap +
+ +
{ + // Prevent users from tabbing out of this element + if (event.key === 'Tab') { + event.preventDefault(); + } + }}> + Keyboard trap +
+ + This container traps keyboard focus by blocking the Tab{' '} + key. + +
+ +
+ Menu item after the trap + (not reachable) +
+
+ +
+

Why this violates the rule

+
    +
  • + Keyboard focus is trapped inside the “Keyboard Trap” container by + preventing Tab key navigation. Users cannot navigate + out of this area using the keyboard and may not be able to reach + other parts of the page. +
  • +
+

2.1.4 Character Key Shortcuts

- Level A + Level A

Allow users to turn off or remap single-key character shortcuts.

- {/* TODO: Add example content here that violates this accessibility rule */} +

+ Pressing the D key at any time deletes your draft. +

+ +
+

Why this violates the rule

+
    +
  • + A single character key (D) triggers an important action + (deleting a draft) regardless of focus. +
  • +
  • + The shortcut is always active and cannot be turned off or + customized. +
  • +
  • + This can cause accidental activation by users with motor + disabilities, screen readers, or speech input. +
  • +
+

2.2.1 Timing Adjustable

- Level A + Level A

Provide user controls to turn off, adjust or extend time limits.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+

This form will expire in 10 seconds.

+
+ + + + + + + + + {isFormExpired && ( +

+ Form expired. Please refresh to try again. +

+ )} +
+
+ +
+

Why this violates the rule

+
    +
  • + Users are given only 10 seconds to complete the form, with no + option to extend or disable the timer. +
  • +
  • + Users with cognitive or motor disabilities may need more time to + complete the form. +
  • +
  • + There is no warning or interaction provided before the time limit + expires. +
  • +
  • + The time constraint is not essential to the functionality (e.g. + it's not a live auction). +
  • +
+

2.2.2 Pause, Stop, Hide

- Level A + Level A

- Provide user controls to pause, stop and hide moving and - auto-updating content. + Provide user controls to pause, stop or hide moving, blinking, + scrolling, or auto-updating content that lasts longer than 5 + seconds.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+

+ 🚨 Breaking News: Accessibility matters. Build it in, don’t bolt + it on. 🚨 +

+
+
+ +
+

Why this violates the rule

+
    +
  • + The banner scrolls automatically and continuously using CSS + animation. +
  • +
  • + There are no controls to pause, stop, or hide the moving text. +
  • +
  • + The movement lasts longer than 5 seconds and is not essential. +
  • +
  • + Continuous motion can be distracting or harmful for users with + cognitive, attention, or vestibular disorders. +
  • +
+

2.3.1 Three Flashes or Below Threshold

- Level A + Level A

No content flashes more than three times per second.

- {/* TODO: Add example content here that violates this accessibility rule */} +
⚠️ Alert! ⚠️
+ +
+

Why this violates the rule

+
    +
  • + The background flashes rapidly — more than three times per second. +
  • +
  • + Flashing content like this can trigger seizures in users with + photosensitive epilepsy. +
  • +
  • + WCAG requires that flashing content remain below the threshold, or + not flash at all. +
  • +
+

2.4.1 Bypass Blocks

- Level A + Level A

Provide a way for users to skip repeated blocks of content.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + +
+

This is the main content area users want to reach quickly.

+ +
+
+ +
+

Why this violates the rule

+
    +
  • + No mechanism (e.g., skip link) is provided to jump past the + navigation menu. Keyboard and screen reader users must tab through + all navigation links every time. +
  • +
  • + This increases the effort and time needed to reach the main + content. +
  • +
+

2.4.2 Page Titled

- Level A + Level A

Use helpful and clear page titles.

- {/* TODO: Add example content here that violates this accessibility rule */} +

See page title

+ +
+

Why this violates the rule

+
    +
  • + The <title> is empty or non-descriptive. +
  • +
  • + Screen readers and browser tabs rely on the page title to convey + page purpose. +
  • +
  • Users can get lost or confused without a meaningful title.
  • +
  • + Page titles are critical for navigation, bookmarking, and search + engine indexing. +
  • +
+

2.4.3 Focus Order

- Level A + Level A

Components receive focus in a logical sequence.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + + +
+ +
+

Why this violates the rule

+
    +
  • + The tabIndex attributes with positive values force + keyboard focus to jump to the “Focus me first” and “Focus me + second” buttons before any other focusable elements on this page. +
  • +
  • + Keyboard navigation does not follow the visual layout or reading + order. +
  • +
  • + This causes confusion and difficulty for keyboard users trying to + navigate the page logically. +
  • +
+

2.4.4 Link Purpose (In Context)

- Level A + Level A

Every link's purpose is clear from its text or context.

- {/* TODO: Add example content here that violates this accessibility rule */} + + +
+

Why this violates the rule

+
    +
  • + The link texts do not describe the purpose of the links. Screen + reader users or users scanning the page quickly can't understand + where the links lead without additional context. +
  • +
  • + Link text should be meaningful on its own or with its immediate + context (like in a paragraph or heading). +
  • +
+

2.4.5 Multiple Ways

- Level AA -

Offer at least two ways to find pages on your website.

+ Level AA +

+ Offer at least two ways to find pages on your website — for example, + through navigation menus, search, or a sitemap. +

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+ {MULTIPLE_WAYS_TABS.map((tab, index) => ( + + ))} +
+ +
+

{activeTab}

+

+ This is the “{activeTab}” content, only accessible via the tab + interface. +

+
+
+ +
+

Why this violates the rule

+
    +
  • + The site provides only one method of locating pages: a basic + navigation menu. +
  • +
  • + There is no search function, sitemap, breadcrumbs, or alternative + way to access pages. +
  • +
  • + Users with different preferences or assistive technologies may + struggle to locate content if they can’t use the main navigation. +
  • +
+

2.4.6 Headings and Labels

- Level AA + Level AA

Headings and labels describe topic or purpose.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+ Look here +
+ + + + +
+ +
+

Why this violates the rule

+
    +
  • + The heading “Look here” is not descriptive or meaningful as a + section heading. +
  • +
  • + The first input has no label, only a placeholder, which disappears + on typing and is not descriptive. +
  • +
  • + The label “Input” is too generic and does not describe what is + expected. +
  • +
  • + Users relying on screen readers may be confused or unable to + understand the purpose of form controls and sections. +
  • +
+

2.4.7 Focus Visible

- Level AA + Level AA

Keyboard focus is visible when used.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + Link +
+ +
+

Why this violates the rule

+
    +
  • The button and link have no focus style.
  • +
  • + Keyboard users cannot see which element is focused when tabbing + through the page. +
  • +
  • + Lack of visible focus causes confusion and poor navigation + experience. +
  • +
+

2.4.11 Focus Not Obscured (Minimum)

- Level AA + Level AA

- When a user interface component is selected, the focus indicator - encompasses the visual presentation of the component, maintains a - contrast ratio of at least 3:1 between its pixels in focused and - unfocused states, and ensures a contrast ratio of at least 3:1 - against adjacent colors. + When navigating with the keyboard, the focused element must at least + be partially visible — not hidden behind fixed or sticky content.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
Sticky Header
+ +
+ {Array.from({length: 3}, (_, i) => ( + + ))} +
+
+ +
+

Why this violates the rule

+
    +
  • + The sticky header overlaps part of the page content. When tabbing + to the first button, the focused element is behind the sticky + header. +
  • +
  • + This fully obscures the keyboard focus and confuses users relying + on visible focus indicators. +
  • +
+

2.5.1 Pointer Gestures

- Level A + Level A

- Multi-point and path-based gestures can be operated with a single - pointer. + Multi-point and path-based gestures are operable with a single + pointer unless the gesture is essential for the functionality.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
Secret content 🤫
+ +
+ Drag me to reveal secret content +
+
+ +
+

Why this violates the rule

+
    +
  • + The component requires a path-based gesture (drag) to reveal the + content and there is no alternative (e.g., a button or tap) for + users who cannot perform dragging. +
  • +
  • + This makes it inaccessible for users with limited mobility or + assistive devices. +
  • +
+

2.5.2 Pointer Cancellation

- Level A -

Functions don't complete on the down-click of a pointer.

+ Level A +

+ Functions don't complete on the down event (e.g.{' '} + mousedown or touchstart) of a pointer. +

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ +
+ +
+

Why this violates the rule

+
    +
  • + The action is triggered immediately on the mousedown{' '} + or touchstart event. +
  • +
  • + Users don’t have the chance to cancel or change their mind before + the action is performed. +
  • +
  • + Accessible implementations should use mouseup,{' '} + click, or touchend to confirm intent. +
  • +
+

2.5.3 Label in Name

- Level A + Level A

- Where a component has a text label, the name of the component also - contains the text displayed. + Where a component has a text label, the accessible name of the + component also contains the text displayed.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+ + +
+
+ +
+

Why this violates the rule

+
    +
  • + The visible label is “Search”, but the accessible name is “Find + something” (set via aria-label). +
  • +
  • + Speech recognition users who say “Click Search” will not be able + to activate this field, because the name does not match the + visible label. +
  • +
  • + The accessible name should contain the visible label text to + ensure consistency and usability. +
  • +
+

2.5.4 Motion Actuation

- Level A + Level A

- Functions operated by motion can also be operated through an - interface and responding to motion can be disabled. + Functions operated by motion (like shaking or tilting a device) can + also be operated through a standard interface and motion-based + operation can be disabled unless the motion is essential.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ Tilt your device to refresh the content. +
+ +
+

Why this violates the rule

+
    +
  • + The feature requires physical motion (tilting the device) to + trigger an action, with no alternative UI control. +
  • +
  • + Users who cannot physically move their device, or who have motion + turned off at the OS level, cannot use this functionality. +
  • +
  • + Motion-triggered actions must also be available through standard + inputs like buttons. +
  • +
+

2.5.7 Dragging Movements

- Level AA + Level AA

All functionality that uses a dragging movement for operation can be achieved by a single pointer without dragging, unless dragging is @@ -446,150 +1796,660 @@ function App() {

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+
+ Drop Zone 1 +
+
+ Drop Zone 2 +
+
+ +
+ +

Drag the blue square into one of the drop zones.

+
+ +
+

Why this violates the rule

+
    +
  • + The draggable element can only be moved using dragging (pointer + gesture). +
  • +
  • + There is no keyboard support to move the element, excluding + keyboard users. +
  • +
  • + Functions that require dragging must have an alternative method of + operation. +
  • +
+

2.5.8 Target Size (Minimum)

- Level AA + Level AA

- Ensure the target of any UI element has 24 by 24 CSS PX target size - or there is enough spacing provided between two targets that have - undersize targets. + Ensure the target of any UI element has 24 by 24 CSS pixels target + size or there is enough spacing provided between two targets that + have undersize targets.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + +
+ +
+

Why this violates the rule

+
    +
  • + The button's dimensions are smaller than 24×24 CSS pixels, making + it difficult to tap or click, especially on touch devices. +
  • +
  • + Small targets increase the chance of accidental activation of + nearby elements. +
  • +
  • + Users with motor impairments may struggle to hit the target + precisely. +
  • +
+

3.1.1 Language of Page

- Level A + Level A

Each webpage has a default human language assigned.

- {/* TODO: Add example content here that violates this accessibility rule */} +

+ See missing lang attribute in the HTML tag. +

+ +
+

Why this violates the rule

+
    +
  • + The HTML document does not specify a lang attribute, + so assistive technologies cannot determine the correct language. +
  • +
  • + Screen readers may use incorrect pronunciation rules, making the + content difficult or impossible to understand. +
  • +
  • + Search engines and translation tools also rely on the{' '} + lang attribute to process content correctly. +
  • +
+

3.1.2 Language of Parts

- Level AA + Level AA

Each part of a webpage has a default human language assigned.

- {/* TODO: Add example content here that violates this accessibility rule */} +

+ This paragraph is in English, but suddenly switches to French without + indicating the language:{' '} + Bonjour, comment allez-vous aujourd'hui ? +

+ +
+

Why this violates the rule

+
    +
  • + The French phrase is not marked with lang="fr", so + screen readers will attempt to pronounce it as English. +
  • +
  • + Users relying on text-to-speech or braille may not understand the + foreign-language content. +
  • +
  • + Accurate language identification is essential for correct + pronunciation, grammar rules, and accessibility support. +
  • +
+

3.2.1 On Focus

- Level A + Level A

Elements do not change when they receive focus.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ +
+ +
+

Why this violates the rule

+
    +
  • + The <input> triggers an alert immediately on + focus, even before any user interaction. +
  • +
  • + Keyboard or assistive tech users may accidentally trigger the + modal just by tabbing through the interface. +
  • +
  • + This can be disorienting and disruptive, especially for users + relying on predictable navigation. +
  • +
+

3.2.2 On Input

- Level A -

Elements do not change when they receive input.

+ Level A +

User input doesn't trigger unexpected context changes.

- {/* TODO: Add example content here that violates this accessibility rule */} +
{ + event.preventDefault(); + alert('Form auto-submitted!'); + }}> + +
+ +
+

Why this violates the rule

+
    +
  • + The form is submitted immediately when the checkbox is checked — + without user confirmation. +
  • +
  • + Users may not expect a form to submit from a checkbox alone. +
  • +
  • + Input changes should require an explicit action (e.g., clicking a + “Submit” button) before triggering major changes. +
  • +
+

3.2.3 Consistent Navigation

- Level AA -

Position menus and standard controls consistently.

+ Level AA +

+ Repeated navigational components appear consistently in the same + order and location across pages. +

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ + + +
+

+ {consistentNavigationPage === 'home' ? 'Home Page' : 'Other Page'} +

+ +
+
+ +
+

Why this violates the rule

+
    +
  • + The same navigation links are presented in a different order + depending on the page. +
  • +
  • + Users may become disoriented or confused by inconsistent + navigation structures. +
  • +
  • + Consistent placement and order help users locate navigation + options predictably. +
  • +
+

3.2.4 Consistent Identification

- Level AA + Level AA

Identify components with the same function consistently.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+

Product A

+ +
+ +
+

Product B

+ +
+ + +

{consistentIdentificationModalContent}

+ +
+
+ +
+

Why this violates the rule

+
    +
  • + Both buttons perform the same function (open product details) but + are labeled inconsistently as “More Info” and “Details”. +
  • +
  • + Inconsistent labels for the same function can confuse users, + especially those using screen readers or keyboard navigation. +
  • +
  • + Identical functionality should always use consistent labeling and + roles across the interface. +
  • +
+

3.2.6 Consistent Help

- Level A -

Help options are presented programmatically in the same order.

+ Level A +

+ Help is provided consistently on each page, with the same label and + placement. +

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+

Page 1

+

Content of page 1

+ +
+ +
+

Page 2

+

Content of page 2

+
+ Support +
+
+
+ +
+

Why this violates the rule

+
    +
  • + The help link on Page 1 is labeled “Need help?” and positioned in + the top-right corner, while on Page 2 it is labeled “Support” and + appears in the bottom-left corner. +
  • +
  • + Users relying on predictable navigation may struggle to locate + support resources when help mechanisms vary in label and + placement. +
  • +
  • + Help options that are repeated across pages should appear in a + consistent location and be identified using the same terminology. +
  • +
+

3.3.1 Error Identification

- Level A + Level A

Identify and describe input errors for users.

- {/* TODO: Add example content here that violates this accessibility rule */} +
{ + event.preventDefault(); + const username = event.currentTarget.username.value; + + if (username.length < 5) { + // Fails validation, but no feedback is shown + console.log( + 'The username is too short, but no message is shown to the user.' + ); + } else { + alert('Form submitted!'); + } + }}> + + + +
+ +
+

Why this violates the rule

+
    +
  • + The form checks if the username is at least 5 characters but + provides no error message or visual indication. +
  • +
  • + Users receive no feedback when their input is invalid, making it + unclear why the form doesn't submit. +
  • +
  • + The error is not described in text and not programmatically + associated with the field. +
  • +
+

3.3.2 Labels or Instruction

- Level A -

Provide labels or instructions for user input.

+ Level A +

Provide clear labels or instructions for user input.

- {/* TODO: Add example content here that violates this accessibility rule */} +
{ + event.preventDefault(); + alert('Form submitted!'); + }}> + + + +
+ +
+

Why this violates the rule

+
    +
  • + The input field is visually missing a label — users don’t know + what data to enter. +
  • +
  • + The placeholder “12345” is not an instruction, and placeholder + text disappears when the user starts typing. +
  • +
  • + The form does not indicate whether the field is required or what + format is expected. +
  • +
+

3.3.3 Error Suggestion

- Level AA -

Suggest corrections when users make mistakes.

+ Level AA +

+ Provide helpful suggestions to users for correcting input errors. +

- {/* TODO: Add example content here that violates this accessibility rule */} +
{ + event.preventDefault(); + const username = event.currentTarget.username.value; + const isValid = validateUsername(username); + + if (isValid) { + alert('Form submitted!'); + } + }}> + + validateUsername(event.currentTarget.value)} + aria-invalid={Boolean(usernameValidationError)} + /> + + {Boolean(usernameValidationError) && ( + + )} + + +
+ +
+

Why this violates the rule

+
    +
  • + The error message states the username is required or invalid but + does not suggest what the user needs to do to correct it. +
  • +
  • + Without guidance, users may not know that the username is required + and should be at least 5 characters long. +
  • +
  • + Providing suggestions helps users fix errors efficiently and + improves accessibility. +
  • +
+

3.3.4 Error Prevention (Legal, Financial, Data)

- Level AA + Level AA

Check, confirm and allow reversal of pages that cause important - commitments. + legal, financial, or data commitments.

- {/* TODO: Add example content here that violates this accessibility rule */} +
{ + event.preventDefault(); + alert('Purchase completed!'); + }}> + + + + +
+ +
+

Why this violates the rule

+
    +
  • + The purchase completes immediately upon submission without a + review or confirmation step. +
  • +
  • + Users have no opportunity to verify or cancel the transaction + before it is finalized. +
  • +
  • + This can cause irreversible legal or financial commitments and + increase the risk of user errors. +
  • +
+

3.3.7 Redundant Entry

- Level A -

Autofill form- fields that repeat across steps.

+ Level A +

Autofill form fields that repeat across steps.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ {redundantEntryStep === 1 && ( +
+ + + +
+ )} + + {redundantEntryStep === 2 && ( +
{ + event.preventDefault(); + alert('Form submitted!'); + }}> + + + + + +
+ + +
+
+ )} +
+ +
+

Why this violates the rule

+
    +
  • + The user must enter the name twice on different steps of the form. +
  • +
  • + This redundant entry wastes time and increases the chance of input + errors. +
  • +
  • + Data should be saved and reused across steps to reduce user + effort. +
  • +
+

3.3.8 Accessible Authentication (Minimum)

- Level AA + Level AA

It states that users must be able to access authentication methods using only a keyboard. This means that the authentication process @@ -597,33 +2457,121 @@ function App() {

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
+ Login +
+
+ +
+

Why this violates the rule

+
    +
  • + The “Login” button is a div with an{' '} + onClick handler but no keyboard support. +
  • +
  • + It does not receive keyboard focus and cannot be activated by + pressing Enter or Space. +
  • +
  • + Users who rely on keyboard-only navigation cannot complete the + authentication process. +
  • +
+

4.1.2 Name, Role, Value

- Level A + Level A

- The name and role of user components can be understood by + The name, role, and value of user components can be understood by technology.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+ {/** + * This is an intentionally inaccessible "checkbox" + * without role, name, and keyboard support. + */} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
setIsChecked(prevState => !prevState)} + style={{ + backgroundColor: isChecked + ? 'var(--ubilabs-green)' + : 'transparent' + }} + /> + I agree to the terms +
+ +
+

Why this violates the rule

+
    +
  • + The checkbox is a plain div with no role or + accessible name. +
  • +
  • + Assistive technologies cannot identify this element as a checkbox + or communicate its checked / unchecked state. +
  • +
  • + Users relying on screen readers or other AT will not understand + this control. +
  • +
+

4.1.3 Status Messages

- Level AA + Level AA

- Make sure that all messages indicating success or errors are read - out by a screen reader. + Make sure that all messages indicating success, errors, or alerts + are read out by a screen reader without requiring user focus.

- {/* TODO: Add example content here that violates this accessibility rule */} +
+
{ + event.preventDefault(); + setShowStatusMessage(true); + }}> + + + +
+ + {showStatusMessage && ( +

Form submitted successfully!

+ )} +
+ +
+

Why this violates the rule

+
    +
  • + The success message is not marked with ARIA attributes such as{' '} + role="status", so screen readers do not announce it. +
  • +
  • + Users relying on screen readers may not know the form was + submitted unless they manually navigate to find the message. +
  • +
  • + Status messages should be programmatically exposed to assistive + technologies without requiring focus or user action. +
  • +
+
); diff --git a/src/index.css b/src/index.css index c9537ee..762251a 100644 --- a/src/index.css +++ b/src/index.css @@ -1,9 +1,20 @@ :root { --ubilabs-anthracite: #1e1e1e; + --ubilabs-gray-800: #3d3d3d; + --ubilabs-gray-700: #5a5a5a; + --ubilabs-gray-500: #a7a7a7; + --ubilabs-gray-200: #eaeaea; --ubilabs-white: #fff; --ubilabs-blue: #5a78fa; + --ubilabs-blue-500: #6e96ff; + --ubilabs-green: #46be78; + --ubilabs-green-800: #1e8250; + --ubilabs-red: #ff6e78; + --ubilabs-red-800: #cd3c50; + --ubilabs-yellow-900: #b47819; --ubilabs-font-color: var(--ubilabs-white); + --ubilabs-font-secondary-color: var(--ubilabs-gray-500); --ubilabs-background-color: var(--ubilabs-anthracite); --ubilabs-font-family: Relative, sans-serif; @@ -25,6 +36,7 @@ @media (prefers-color-scheme: light) { :root { --ubilabs-font-color: var(--ubilabs-anthracite); + --ubilabs-font-secondary-color: var(--ubilabs-gray-700); --ubilabs-background-color: var(--ubilabs-white); } } @@ -44,32 +56,49 @@ body { font-family: var(--ubilabs-font-family); font-size: var(--ubilabs-default-font-size); line-height: var(--ubilabs-default-line-height); + text-wrap: balance; } h1, +.h1, h2, +.h2, h3, +.h3, h4, +.h4, h5, +.h5, h6, +.h6, p { margin-block-start: var(--ubilabs-default-font-margin-block-start); margin-block-end: var(--ubilabs-default-font-margin-block-end); } -h1 { +h1, +.h1 { font-weight: 500; font-size: 3rem; /* 48px */ + font-size: min(3rem, 12vw); /* 48px, 12vw */ line-height: 1.2; letter-spacing: -0.005em; } -h2 { +h2, +.h2 { font-weight: 500; font-size: 1.5rem; /* 24px */ line-height: 1.333; } +h3, +.h3 { + font-weight: 500; + font-size: 1.25rem; /* 20px */ + line-height: 1.6; +} + p { font-size: var(--ubilabs-default-font-size); line-height: var(--ubilabs-default-line-height); @@ -81,5 +110,77 @@ a { a:hover, a:focus-visible { - color: var(--ubilabs-blue); + color: var(--ubilabs-blue-500); +} + +button, +.button { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-width: 2.5rem; + min-height: 2.5rem; + padding: 0.5em 1em; + background: none; + outline: none; + border: none; + border-radius: 2px; + background-color: #3c55dc; + color: var(--ubilabs-white); + font: var(--ubilabs-default-font); + font-weight: 500; + cursor: default; +} + +button:not([disabled]):focus-visible, +.button:not([disabled]):focus-visible { + box-shadow: var(--ubilabs-focus-box-shadow); +} + +button:not([disabled]):hover, +button:not([disabled]):focus-visible, +.button:not([disabled]):hover, +.button:not([disabled]):focus-visible { + background-color: #1e32be; +} + +button:not([disabled]):active, +.button:not([disabled]):active { + background-color: #3c55dc; +} + +button[disabled], +.button.disabled { + background-color: #5a5a5a; + color: #dadada; +} + +input:not([type]), +input[type='date'], +input[type='datetime-local'], +input[type='email'], +input[type='month'], +input[type='number'], +input[type='password'], +input[type='search'], +input[type='tel'], +input[type='text'], +input[type='time'], +input[type='url'], +input[type='week'] { + padding: 0.5rem; +} + +li:not(:last-of-type) { + margin-block-end: 0.5rem; +} + +li > ul { + margin-block-start: 0.25rem; +} + +li > ul > li:not(:last-of-type) { + margin-block-end: 0.25rem; }